refine style store runtime clarity and card layout

This commit is contained in:
cooper committed 2026-05-13 19:18:58 +08:00
1 parent 0c6cc8b292
commit 16dd99cf67
6 files changed
+189 -160

No files matched your search

@@ -880,6 +880,12 @@ impl Coordinator {
pack_name: style_pack.name.clone(),
pack_prompt: style_pack.prompt.clone(),
pack_prompt_chars: style_pack.prompt.chars().count(),
context_premise: single_turn.context_premise.clone(),
context_premise_chars: single_turn.context_premise.chars().count(),
hotword_block: single_turn.hotword_block.clone(),
hotword_block_chars: single_turn.hotword_block.chars().count(),
history_instruction: multi_turn.history_instruction.clone(),
history_instruction_chars: multi_turn.history_instruction.chars().count(),
single_turn_prompt: single_turn.effective_system_prompt.clone(),
single_turn_prompt_chars: single_turn.effective_system_prompt.chars().count(),
multi_turn_prompt: multi_turn.effective_system_prompt.clone(),
+36 -3
View File
@@ -272,6 +272,9 @@ pub struct OpenAICompatibleLLMProvider {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PolishSystemPromptAssembly {
pub context_premise: String,
pub hotword_block: String,
pub history_instruction: String,
pub effective_system_prompt: String,
pub includes_context_premise: bool,
pub includes_hotword_block: bool,
@@ -1667,15 +1670,25 @@ pub(crate) fn assemble_polish_system_prompt(
front_app,
has_prior_turns,
);
let includes_hotword_block = !hotwords.iter().all(|value| value.trim().is_empty());
let includes_context_premise = context_premise(
let context_premise = context_premise(
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
.is_some();
.unwrap_or_default();
let hotword_block = compose_hotword_block_preview(hotwords);
let history_instruction = if has_prior_turns {
prompts::polish_context_instruction().to_string()
} else {
String::new()
};
let includes_hotword_block = !hotword_block.is_empty();
let includes_context_premise = !context_premise.is_empty();
PolishSystemPromptAssembly {
context_premise,
hotword_block,
history_instruction,
effective_system_prompt,
includes_context_premise,
includes_hotword_block,
@@ -1743,6 +1756,26 @@ fn compose_system_prompt(style_system_prompt: &str, hotwords: &[String]) -> Stri
)
}
fn compose_hotword_block_preview(hotwords: &[String]) -> String {
let cleaned: Vec<String> = hotwords
.iter()
.map(|h| h.trim().to_string())
.filter(|h| !h.is_empty())
.collect();
if cleaned.is_empty() {
return String::new();
}
let bullets = cleaned
.iter()
.map(|h| format!("- {}", h))
.collect::<Vec<_>>()
.join("\n");
format!(
"热词(用户希望以下写法在输出中保持准确;当转写中出现这些词的同音 / 近形词识别时,优先按上述写法输出,不做无关词的机械替换):\n{}",
bullets
)
}
fn extract_assistant_content(body: &str) -> Result<String, LLMError> {
let json: Value = serde_json::from_str(body)
.map_err(|e| LLMError::ParseError(format!("not valid JSON: {}", e)))?;
+6
View File
@@ -276,6 +276,12 @@ pub struct StylePackRuntimeDiagnostics {
pub pack_name: String,
pub pack_prompt: String,
pub pack_prompt_chars: usize,
pub context_premise: String,
pub context_premise_chars: usize,
pub hotword_block: String,
pub hotword_block_chars: usize,
pub history_instruction: String,
pub history_instruction_chars: usize,
pub single_turn_prompt: String,
pub single_turn_prompt_chars: usize,
pub multi_turn_prompt: String,
+14 -9
View File
@@ -292,15 +292,14 @@ function cloneMockStylePacks(): StylePack[] {
function composeMockStylePackRuntimeDiagnostics(stylePack: StylePack): StylePackRuntimeDiagnostics {
const trimmedPrompt = stylePack.prompt.trimEnd();
const contextLines = mockSettings.workingLanguages.length
? [`# Context`, `Working languages: ${mockSettings.workingLanguages.join(', ')}`]
: [];
const contextPremise = mockSettings.workingLanguages.length
? ['# Context', `Working languages: ${mockSettings.workingLanguages.join(', ')}`].join('\n')
: '';
const hotwordLines = [`GitHub`, `OpenLess`];
const hotwordBlock =
hotwordLines.length > 0
? ['', 'Hotwords (keep the spelling below when they appear in the transcript):', ...hotwordLines.map(word => `- ${word}`)]
: [];
const singleTurnPrompt = [...contextLines, trimmedPrompt, ...hotwordBlock].filter(Boolean).join('\n\n');
const hotwordBlock = hotwordLines.length > 0
? ['Hotwords (keep the spelling below when they appear in the transcript):', ...hotwordLines.map(word => `- ${word}`)].join('\n')
: '';
const singleTurnPrompt = [contextPremise, trimmedPrompt, hotwordBlock].filter(Boolean).join('\n\n');
const historyInstruction = 'When prior turns exist, do not repeat previous assistant outputs. Only polish the current transcript.';
const multiTurnPrompt = `${singleTurnPrompt}\n\n${historyInstruction}`;
return {
@@ -308,6 +307,12 @@ function composeMockStylePackRuntimeDiagnostics(stylePack: StylePack): StylePack
packName: stylePack.name,
packPrompt: stylePack.prompt,
packPromptChars: stylePack.prompt.length,
contextPremise,
contextPremiseChars: contextPremise.length,
hotwordBlock,
hotwordBlockChars: hotwordBlock.length,
historyInstruction,
historyInstructionChars: historyInstruction.length,
singleTurnPrompt,
singleTurnPromptChars: singleTurnPrompt.length,
multiTurnPrompt,
@@ -315,7 +320,7 @@ function composeMockStylePackRuntimeDiagnostics(stylePack: StylePack): StylePack
workingLanguages: [...mockSettings.workingLanguages],
hotwords: [...hotwordLines],
contextWindowMinutes: mockSettings.polishContextWindowMinutes,
includesContextPremise: contextLines.length > 0,
includesContextPremise: Boolean(contextPremise),
includesHotwordBlock: hotwordLines.length > 0,
includesHistoryInstruction: true,
previewOmitsFrontApp: true,
+6
View File
@@ -181,6 +181,12 @@ export interface StylePackRuntimeDiagnostics {
packName: string;
packPrompt: string;
packPromptChars: number;
contextPremise: string;
contextPremiseChars: number;
hotwordBlock: string;
hotwordBlockChars: number;
historyInstruction: string;
historyInstructionChars: number;
singleTurnPrompt: string;
singleTurnPromptChars: number;
multiTurnPrompt: string;
+121 -148
View File
@@ -77,45 +77,45 @@ export function Style() {
kicker: 'STYLE PACKS',
title: isEnglish ? 'Style Packs' : '风格包',
desc: isEnglish
? 'Manage output styles as pluggable Style Packs. Built-in and imported packs share the same runtime prompt, ZIP exchange format, and repolish path.'
: '把输出风格作为可插拔的 Style Pack 来管理。内置风格和导入风格共用同一套运行时 prompt、ZIP 交换格式和 repolish 链路。',
? 'Manage local style packs.'
: '管理本地风格包。',
loadFailed: (message: string) => (isEnglish ? `Failed to load style packs: ${message}` : `加载风格包失败:${message}`),
importZip: isEnglish ? 'Import ZIP' : '导入 ZIP',
exportZip: isEnglish ? 'Export ZIP' : '导出 ZIP',
exportShort: isEnglish ? 'Export' : '导出',
builtin: isEnglish ? 'Built-in' : '内置',
imported: isEnglish ? 'Imported' : '导入',
active: isEnglish ? 'Active' : '当前启用',
enabled: isEnglish ? 'Enabled' : '已启用',
disabled: isEnglish ? 'Disabled' : '已停用',
activate: isEnglish ? 'Activate' : '启用',
enable: isEnglish ? 'Enable' : '启用',
disable: isEnglish ? 'Disable' : '停用',
active: isEnglish ? 'Active' : '当前',
enabled: isEnglish ? 'In Rotation' : '已加入轮换',
disabled: isEnglish ? 'Out of Rotation' : '未加入轮换',
activate: isEnglish ? 'Activate' : '激活',
enable: isEnglish ? 'Rotation ON' : '轮换 ON',
disable: isEnglish ? 'Rotation OFF' : '轮换 OFF',
edit: isEnglish ? 'Edit' : '编辑',
closeEditor: isEnglish ? 'Close' : '关闭',
unsaved: isEnglish ? 'Unsaved' : '未保存',
listTitle: isEnglish ? 'Local Packs' : '本地风格包',
listDesc: isEnglish
? 'Browse and manage packs like a local storefront. Keep this page focused on cards, then open a dedicated editor only when you need to change prompt details.'
: '像本地商店一样浏览和管理风格包。主页面只保留卡片浏览,真正修改 prompt 和示例时再打开独立编辑面板。',
? 'Browse and switch packs.'
: '浏览和切换风格包。',
listCount: (count: number) => (isEnglish ? `${count} packs` : `${count} 个风格包`),
save: isEnglish ? 'Save Pack' : '保存风格包',
revert: isEnglish ? 'Discard Changes' : '放弃修改',
save: isEnglish ? 'Save' : '保存',
revert: isEnglish ? 'Revert' : '撤销',
saveSuccess: isEnglish ? 'Style pack saved.' : '风格包已保存',
saveFailed: (message: string) => (isEnglish ? `Failed to save style pack: ${message}` : `保存风格包失败:${message}`),
activateSuccess: (name: string) => (isEnglish ? `Activated "${name}".` : `已启用“${name}”`),
activateFailed: (message: string) => (isEnglish ? `Failed to activate style pack: ${message}` : `启用风格包失败:${message}`),
enableSuccess: (name: string) => (isEnglish ? `Enabled "${name}".` : `已启用“${name}”`),
disableSuccess: (name: string) => (isEnglish ? `Disabled "${name}".` : `已停用“${name}”`),
toggleFailed: (message: string) => (isEnglish ? `Failed to change pack status: ${message}` : `切换启用状态失败:${message}`),
activateSuccess: (name: string) => (isEnglish ? `Set "${name}" as current.` : `已将“${name}”设为当前风格`),
activateFailed: (message: string) => (isEnglish ? `Failed to set current style pack: ${message}` : `设为当前风格失败:${message}`),
enableSuccess: (name: string) => (isEnglish ? `Added "${name}" to rotation.` : `已将“${name}”加入轮换`),
disableSuccess: (name: string) => (isEnglish ? `Removed "${name}" from rotation.` : `已将“${name}”移出轮换`),
toggleFailed: (message: string) => (isEnglish ? `Failed to change rotation status: ${message}` : `切换轮换状态失败:${message}`),
importSuccess: (name: string) => (isEnglish ? `Imported "${name}".` : `已导入“${name}”`),
importFailed: (message: string) => (isEnglish ? `Failed to import ZIP: ${message}` : `导入 ZIP 失败:${message}`),
exportSuccess: (path: string) => (isEnglish ? `Exported to ${path}` : `已导出到 ${path}`),
exportFailed: (message: string) => (isEnglish ? `Failed to export ZIP: ${message}` : `导出 ZIP 失败:${message}`),
resetBuiltin: isEnglish ? 'Reset Built-in Pack' : '重置内置风格',
resetBuiltin: isEnglish ? 'Reset' : '重置',
resetSuccess: (name: string) => (isEnglish ? `Reset "${name}".` : `已重置“${name}”`),
resetFailed: (message: string) => (isEnglish ? `Failed to reset pack: ${message}` : `重置风格包失败:${message}`),
deleteImported: isEnglish ? 'Delete Imported Pack' : '删除导入风格',
deleteImported: isEnglish ? 'Delete' : '删除',
deleteConfirm: (name: string) => (isEnglish
? `Delete "${name}"? This cannot be undone.`
: `确定删除“${name}”吗?删除后无法恢复。`),
@@ -125,18 +125,17 @@ export function Style() {
summaryBuiltinHint: isEnglish ? 'Default product semantics with one-click reset.' : '跟随产品默认语义,可一键重置到官方基线。',
summaryImported: isEnglish ? 'Imported Packs' : '导入风格',
summaryImportedHint: isEnglish ? 'Installed from ZIP and fully portable.' : '来自 ZIP 包,可启用、编辑、导出和删除。',
summaryEnabled: isEnglish ? 'Rotation Ready' : '当前可轮换',
summaryEnabled: isEnglish ? 'In Rotation' : '已加入轮换',
summaryCurrent: (name: string) => (isEnglish ? `Current: ${name}` : `当前启用:${name}`),
summaryCurrentEmpty: isEnglish ? 'No pack selected yet' : '还没有选中风格包',
summaryFocused: (name: string) => (isEnglish ? `Focused: ${name}` : `当前浏览:${name}`),
editorTitle: isEnglish ? 'Style Pack Editor' : '风格包编辑面板',
editorTitle: isEnglish ? 'Edit Pack' : '编辑风格',
editorDesc: isEnglish
? 'Edit the full runtime prompt, examples, tags, export, reset, and metadata here without crowding the main store view.'
: '在这里集中编辑完整运行时 prompt、示例、标签、导入导出和重置操作,不再占满主列表空间。',
? 'Edit this pack.'
: '编辑当前风格包。',
metaTitle: isEnglish ? 'Installation Info' : '安装信息',
metaSource: isEnglish ? 'Source' : '来源',
metaBaseMode: isEnglish ? 'Base Mode' : '基础模式',
metaStatus: isEnglish ? 'Status' : '状态',
metaStatus: isEnglish ? 'Rotation' : '轮换状态',
metaUpdatedAt: isEnglish ? 'Updated' : '更新时间',
fieldName: isEnglish ? 'Name' : '名称',
fieldAuthor: isEnglish ? 'Author' : '作者',
@@ -148,53 +147,39 @@ export function Style() {
fieldModel: isEnglish ? 'Recommended Model (Metadata)' : '推荐模型(仅元数据)',
fieldModelPlaceholder: isEnglish ? 'Optional, e.g. gpt-4.1 / deepseek-v3' : '可选,例如 gpt-4.1 / deepseek-v3',
fieldModelHint: isEnglish
? 'Advisory metadata only. It is exported in the pack manifest for humans, but it never switches the actual LLM provider or model.'
: '这是给人看的建议元数据,会写入风格包 manifest,方便分享时说明适配模型;它不会自动切换实际使用的 LLM provider 或 model。',
? 'Metadata only. Does not switch model.'
: '仅作说明,不会切换实际模型。',
fieldCompatibility: isEnglish ? 'Compatible App Version' : '兼容版本',
fieldCompatibilityPlaceholder: isEnglish ? 'Optional, e.g. >=1.3.0' : '可选,例如 >=1.3.0',
fullPromptTitle: isEnglish ? 'Style Pack Prompt Body' : 'Style Pack Prompt 主体',
fullPromptTitle: isEnglish ? 'System Prompt' : 'System Prompt',
fullPromptHint: isEnglish
? 'This textarea owns the pack’s full style definition. OpenLess no longer swaps in a hidden built-in mode prompt and then appends your text.'
: '这里编辑的是这套风格包自己拥有的完整风格定义。OpenLess 不再走“隐藏内置 prompt + 末尾追加文本”的旧语义。',
runtimeTitle: isEnglish ? 'Runtime Assembly' : '运行时组装说明',
? 'The prompt owned by this pack.'
: '这就是这套风格包自己的 Prompt。',
runtimeTitle: isEnglish ? 'OpenLess Runtime Directives' : 'OpenLess 运行时附加指令',
runtimeDesc: isEnglish
? 'At runtime the pack prompt stays as the style body. OpenLess may only prepend context premises and append helper guardrails for the current session.'
: '运行时仍以这段 pack prompt 作为风格主体;OpenLess 只会按当前会话额外注入上下文前提与辅助约束,不会再替换成另一套隐藏风格 prompt。',
runtimePackStageTitle: isEnglish ? 'Pack-owned prompt body' : 'Pack 自有 prompt 主体',
runtimePackStageDesc: isEnglish
? 'This editable text is the style definition that both polish and repolish share.'
: '你在上面编辑的文本就是这套风格包拥有的风格主体,polish 和 repolish 共用它。',
runtimeContextStageTitle: isEnglish ? 'Context premise' : '上下文前提',
runtimeContextStageDesc: isEnglish
? 'Prepended when working-language or output-language preferences are available. This is session context, not another style prompt.'
: '当工作语言、字形偏好或输出语言偏好存在时,会前置注入这一段。这是会话上下文,不是另一套风格 prompt。',
runtimeHotwordStageTitle: isEnglish ? 'Hotword spelling hints' : '热词拼写提示',
runtimeHotwordStageDesc: isEnglish
? 'Appended when enabled vocabulary entries exist so names and terms keep the expected spelling.'
: '当用户词库里存在已启用热词时,会追加这一段,帮助专名和术语保持预期写法。',
runtimeHistoryStageTitle: isEnglish ? 'Multi-turn history guardrail' : '多轮历史保护段',
runtimeHistoryStageDesc: isEnglish
? 'Only added for live polish when prior turns are present. Repolish does not use this segment.'
: '只有实时 polish 在存在 prior turns 时才会追加这段;History 里的 repolish 不会带它。',
runtimePreviewTitle: isEnglish ? 'Current Runtime Preview' : '当前运行时预览',
runtimePreviewDesc: isEnglish
? 'Preview generated from the current app preferences and enabled hotwords. It intentionally omits the frontmost-app label so the editor window itself does not pollute the result.'
: '这个预览按当前应用设置和已启用热词生成。为了避免把编辑器窗口本身误当成真实输入目标,预览会刻意省略前台 app 名称。',
runtimePreviewSingle: isEnglish ? 'Single-turn polish / repolish' : '单轮 polish / repolish',
runtimePreviewMulti: isEnglish ? 'Live polish with prior turns' : '带上下文的实时 polish',
? 'Read-only runtime helpers.'
: '只读的运行时辅助项。',
runtimeDirectiveContextTitle: isEnglish ? 'Context premise' : '上下文前提',
runtimeDirectiveContextDesc: isEnglish ? 'From language and app context' : '来自语言与应用上下文',
runtimeDirectiveContextEmpty: isEnglish ? 'Not added in the current preview.' : '当前不会附加',
runtimeDirectiveHotwordTitle: isEnglish ? 'Hotword block' : '热词提示段',
runtimeDirectiveHotwordDesc: isEnglish ? 'From enabled hotwords' : '来自已启用热词',
runtimeDirectiveHotwordEmpty: isEnglish ? 'Not added in the current preview.' : '当前不会附加',
runtimeDirectiveHistoryTitle: isEnglish ? 'Multi-turn history guardrail' : '多轮历史保护段',
runtimeDirectiveHistoryDesc: isEnglish ? 'Only for live multi-turn polish' : '仅用于实时多轮 polish',
runtimeDirectiveHistoryEmpty: isEnglish ? 'Only added when prior turns exist.' : '只有存在 prior turns 时才会附加',
runtimeDirectiveActive: isEnglish ? 'Active' : '当前生效',
runtimeDirectiveInactive: isEnglish ? 'Inactive' : '当前未生效',
runtimePreviewFailed: (message: string) => (isEnglish ? `Failed to build runtime preview: ${message}` : `生成运行时预览失败:${message}`),
runtimePreviewOmittedFrontApp: isEnglish ? 'Front-app label omitted in preview.' : '预览已省略前台 app 标签。',
runtimePromptChars: (count: number) => (isEnglish ? `${count} chars` : `${count} 字符`),
runtimeHotwordCount: (count: number) => (isEnglish ? `${count} hotwords` : `${count} 个热词`),
runtimeContextWindow: (minutes: number) => (isEnglish ? `${minutes} min context window` : `${minutes} 分钟上下文窗口`),
runtimePreviewOmittedFrontApp: isEnglish ? 'Preview omits the front-app label.' : '预览已省略前台 app 标签。',
examplesTitle: isEnglish ? 'Effect Examples' : '效果示例',
examplesDesc: isEnglish
? 'Present input/output pairs like a product detail page. They will be exported into examples.json.'
: '像商品详情页一样展示输入和输出。导出 ZIP 时会一起写入 examples.json。',
? 'Exported with the pack.'
: '会随风格包一起导出。',
addExample: isEnglish ? 'Add Example' : '新增示例',
examplesEmpty: isEnglish
? 'This pack has no examples yet. Add at least one input/output pair before sharing it.'
: '这个风格包还没有示例。建议至少补一组输入/输出,方便导出后被别人理解和复用。',
? 'No examples yet.'
: '还没有示例。',
exampleTitlePlaceholder: (index: number) => (isEnglish ? `Example ${index} title` : `示例 ${index} 标题`),
exampleInput: isEnglish ? 'Input' : '输入',
exampleOutput: isEnglish ? 'Output' : '输出',
@@ -579,7 +564,6 @@ export function Style() {
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', marginTop: 4, maxWidth: 760 }}>{copy.listDesc}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
{selectedPack && <Pill tone="default">{copy.summaryFocused(selectedPack.name)}</Pill>}
<Pill tone="outline">{copy.listCount(packs.length)}</Pill>
</div>
</div>
@@ -592,6 +576,8 @@ export function Style() {
<div
key={pack.id}
style={{
display: 'flex',
flexDirection: 'column',
textAlign: 'left',
border: '0.5px solid',
borderColor: selected || pack.active ? 'var(--ol-blue)' : 'var(--ol-line)',
@@ -603,18 +589,24 @@ export function Style() {
boxShadow: selected || pack.active ? '0 0 0 3px var(--ol-blue-ring)' : 'none',
cursor: 'default',
opacity: pack.enabled ? 1 : 0.72,
minHeight: 204,
transition: 'border-color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft), opacity 0.18s var(--ol-motion-soft)',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 10 }}>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-ink)' }}>{pack.name}</div>
<Pill tone={pack.kind === 'builtin' ? 'outline' : 'blue'} size="sm">
{pack.kind === 'builtin' ? copy.builtin : copy.imported}
</Pill>
{pack.active && <Pill tone="dark" size="sm">{copy.active}</Pill>}
{!pack.enabled && <Pill tone="default" size="sm">{copy.disabled}</Pill>}
<div style={{ minWidth: 42, minHeight: 24, display: 'flex', alignItems: 'center' }}>
{pack.active ? (
<Pill tone="dark" size="sm">{copy.active}</Pill>
) : !pack.enabled ? (
<Pill tone="default" size="sm">{copy.disabled}</Pill>
) : null}
</div>
</div>
<div
style={{
@@ -625,6 +617,8 @@ export function Style() {
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
overflow: 'hidden',
marginTop: 8,
minHeight: 60,
}}
>
{pack.description}
@@ -655,24 +649,14 @@ export function Style() {
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minHeight: 24, marginBottom: 12 }}>
<Pill tone={modeTone(pack.baseMode)} size="sm">{t(`style.modes.${pack.baseMode}.name`)}</Pill>
<Pill tone="default" size="sm">{copy.examplesCount(pack.examples.length)}</Pill>
{pack.tags.slice(0, 2).map(tag => (
{pack.tags.slice(0, 1).map(tag => (
<Pill key={`${pack.id}-${tag}`} tone="default" size="sm">{tag}</Pill>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 14 }}>
<Pill tone="outline" size="sm">
{copy.metaSource}: {pack.kind === 'builtin' ? copy.builtin : copy.imported}
</Pill>
<Pill tone="outline" size="sm">
{copy.metaStatus}: {pack.enabled ? copy.enabled : copy.disabled}
</Pill>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 'auto' }}>
<Btn
size="sm"
variant={pack.active ? 'soft' : 'ghost'}
@@ -789,6 +773,13 @@ export function Style() {
<Btn variant="ghost" icon="archive" onClick={() => void handleExportZip()} disabled={busy === 'exporting'}>
{copy.exportZip}
</Btn>
<Btn
variant="ghost"
disabled={busy === 'toggling'}
onClick={() => void handleToggleEnabled(draft)}
>
{draft.enabled ? copy.disable : copy.enable}
</Btn>
<Btn
variant={draft.active ? 'soft' : 'blue'}
icon="check"
@@ -871,7 +862,7 @@ export function Style() {
<label style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink)' }}>{copy.fullPromptTitle}</span>
<Pill tone="default" size="sm">{copy.runtimePromptChars(draft.prompt.length)}</Pill>
<Pill tone="default" size="sm">{isEnglish ? `${draft.prompt.length} chars` : `${draft.prompt.length} 字符`}</Pill>
</div>
<span style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.55 }}>{copy.fullPromptHint}</span>
<textarea
@@ -893,37 +884,36 @@ export function Style() {
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ol-ink)' }}>{copy.runtimeTitle}</div>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginTop: 4, lineHeight: 1.6 }}>{copy.runtimeDesc}</div>
</div>
{runtimePreview && (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Pill tone="default" size="sm">{copy.runtimePromptChars(runtimePreview.singleTurnPromptChars)}</Pill>
<Pill tone="outline" size="sm">{copy.runtimeHotwordCount(runtimePreview.hotwords.length)}</Pill>
</div>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 10, marginBottom: 14 }}>
<RuntimeStageCard title={copy.runtimePackStageTitle} desc={copy.runtimePackStageDesc} tone="default" />
<RuntimeStageCard title={copy.runtimeContextStageTitle} desc={copy.runtimeContextStageDesc} tone={runtimePreview?.includesContextPremise ? 'blue' : 'outline'} />
<RuntimeStageCard title={copy.runtimeHotwordStageTitle} desc={copy.runtimeHotwordStageDesc} tone={runtimePreview?.includesHotwordBlock ? 'blue' : 'outline'} />
<RuntimeStageCard title={copy.runtimeHistoryStageTitle} desc={copy.runtimeHistoryStageDesc} tone={runtimePreview?.includesHistoryInstruction ? 'blue' : 'outline'} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 12 }}>
<ReadonlyPromptPanel
title={copy.runtimePreviewSingle}
body={runtimePreview?.singleTurnPrompt ?? ''}
countLabel={copy.runtimePromptChars(runtimePreview?.singleTurnPromptChars ?? 0)}
footer={runtimePreview?.previewOmitsFrontApp ? copy.runtimePreviewOmittedFrontApp : ''}
<div style={{ display: 'grid', gap: 8, marginBottom: 8 }}>
<DirectiveRow
title={copy.runtimeDirectiveContextTitle}
detail={copy.runtimeDirectiveContextDesc}
active={Boolean(runtimePreview?.contextPremise)}
activeLabel={copy.runtimeDirectiveActive}
inactiveLabel={copy.runtimeDirectiveInactive}
inactiveHint={copy.runtimeDirectiveContextEmpty}
/>
<ReadonlyPromptPanel
title={copy.runtimePreviewMulti}
body={runtimePreview?.multiTurnPrompt ?? ''}
countLabel={copy.runtimePromptChars(runtimePreview?.multiTurnPromptChars ?? 0)}
footer={runtimePreview ? copy.runtimeContextWindow(runtimePreview.contextWindowMinutes) : ''}
<DirectiveRow
title={copy.runtimeDirectiveHotwordTitle}
detail={copy.runtimeDirectiveHotwordDesc}
active={Boolean(runtimePreview?.hotwordBlock)}
activeLabel={copy.runtimeDirectiveActive}
inactiveLabel={copy.runtimeDirectiveInactive}
inactiveHint={copy.runtimeDirectiveHotwordEmpty}
/>
<DirectiveRow
title={copy.runtimeDirectiveHistoryTitle}
detail={copy.runtimeDirectiveHistoryDesc}
active={Boolean(runtimePreview?.historyInstruction)}
activeLabel={copy.runtimeDirectiveActive}
inactiveLabel={copy.runtimeDirectiveInactive}
inactiveHint={copy.runtimeDirectiveHistoryEmpty}
/>
</div>
<div style={{ fontSize: 11.5, color: runtimePreviewError ? 'var(--ol-red, #b91c1c)' : 'var(--ol-ink-4)', marginTop: 10, lineHeight: 1.55 }}>
{runtimePreviewError ? copy.runtimePreviewFailed(runtimePreviewError) : copy.runtimePreviewDesc}
{runtimePreviewError ? copy.runtimePreviewFailed(runtimePreviewError) : copy.runtimePreviewOmittedFrontApp}
</div>
</Card>
@@ -1074,58 +1064,41 @@ function MetaItem({ label, value }: { label: string; value: string }) {
);
}
function RuntimeStageCard({
function DirectiveRow({
title,
desc,
tone,
detail,
active,
activeLabel,
inactiveLabel,
inactiveHint,
}: {
title: string;
desc: string;
tone: 'default' | 'blue' | 'outline';
detail: string;
active: boolean;
activeLabel: string;
inactiveLabel: string;
inactiveHint: string;
}) {
return (
<div
style={{
borderRadius: 14,
border: tone === 'blue' ? '0.5px solid rgba(37,99,235,0.18)' : '0.5px solid rgba(148,163,184,0.22)',
background: tone === 'blue' ? 'rgba(239,246,255,0.88)' : 'rgba(255,255,255,0.92)',
padding: 12,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Pill tone={tone} size="sm">{title}</Pill>
</div>
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', lineHeight: 1.6 }}>{desc}</div>
</div>
);
}
function ReadonlyPromptPanel({
title,
body,
footer,
countLabel,
}: {
title: string;
body: string;
footer?: string;
countLabel: string;
}) {
return (
<div
style={{
borderRadius: 14,
border: '0.5px solid rgba(148,163,184,0.22)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
padding: '10px 12px',
borderRadius: 12,
border: '0.5px solid rgba(148,163,184,0.2)',
background: 'rgba(255,255,255,0.92)',
padding: 12,
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink)' }}>{title}</div>
<Pill tone="default" size="sm">{countLabel}</Pill>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--ol-ink)' }}>{title}</div>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.5, marginTop: 2 }}>
{active ? detail : inactiveHint}
</div>
</div>
<textarea readOnly value={body} style={{ ...textareaStyle, minHeight: 150, background: 'rgba(248,250,252,0.96)' }} />
{footer && <div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.5 }}>{footer}</div>}
<Pill tone={active ? 'blue' : 'outline'} size="sm">{active ? activeLabel : inactiveLabel}</Pill>
</div>
);
}