diff options
Diffstat (limited to 'frontend/src/pages/ProjectStrategyTab.svelte')
| -rw-r--r-- | frontend/src/pages/ProjectStrategyTab.svelte | 94 |
1 files changed, 57 insertions, 37 deletions
diff --git a/frontend/src/pages/ProjectStrategyTab.svelte b/frontend/src/pages/ProjectStrategyTab.svelte index e4269e3..53c8bea 100644 --- a/frontend/src/pages/ProjectStrategyTab.svelte +++ b/frontend/src/pages/ProjectStrategyTab.svelte @@ -9,6 +9,7 @@ import { Alert } from '$lib/components/ui/alert'; import { Badge } from '$lib/components/ui/badge'; import * as Card from '$lib/components/ui/card'; +import * as Textarea from '$lib/components/ui/textarea'; import * as Table from '$lib/components/ui/table'; import { projectsStore } from '../lib/listsStore.svelte'; import { @@ -262,28 +263,34 @@ } </script> -<div class="flex flex-col gap-3"> - <Status busy={initBusy} error={initErr} busyText="加载策略草稿…" empty={null} /> - - {#if !initBusy && !initErr} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <div class="flex flex-wrap justify-between items-center gap-2"> - <h2 class="text-lg font-semibold">策略代码(Backtrader Python)</h2> - <span class="text-sm text-muted-foreground" role="status"> +<div class="flex flex-col gap-4 md:gap-6"> + {#if initBusy} + <Status busy skeletonRows={5} busyText="加载策略草稿…" empty={null} /> + {:else if initErr} + <Status busy={false} error={initErr} empty={null}> + {#snippet errorCta()} + <Button onclick={() => void init()}>重新加载</Button> + {/snippet} + </Status> + {:else} + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex flex-wrap justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">策略代码(Backtrader Python)</h2> + <span class="text-xs text-muted-foreground" role="status" data-testid="save-status"> {saveStatus === 'saved' && `已保存${saveMsg ? ` · ${saveMsg}` : ''}`} {#if saveStatus === 'dirty'}<Badge variant="outline">有未保存修改</Badge>{/if} {#if saveStatus === 'saving'}<Badge variant="secondary">保存中…</Badge>{/if} {#if saveStatus === 'conflict'}<Badge variant="destructive">冲突</Badge>{/if} </span> </div> - <p class="text-sm text-muted-foreground"> - 编辑器修改会自动保存(约 0.8 秒停顿后提交)。草稿代 {baseGeneration} · 回测与获取 AI 建议使用当前草稿快照 + <p class="text-xs text-muted-foreground m-0"> + 编辑器修改会自动保存(约 0.8 秒停顿后提交)。草稿代 {baseGeneration} · 回测与 AI 使用当前草稿快照 </p> <CodeEditor value={code} onInput={onEditorInput} readOnly={false} label="策略源码" height={430} /> - <div class="flex flex-wrap justify-between items-center gap-2"> + <div class="flex flex-wrap justify-between items-center gap-4"> <Input bind:value={snapshotMessage} - placeholder="快照说明,例如:加入RSI过滤器(可选)" + placeholder="快照说明(可选),例如:加入RSI过滤器" class="flex-[2_1_240px] min-w-[200px]" onkeydown={(ev) => { if (ev.key === 'Enter') void snapshot(); @@ -300,7 +307,11 @@ 草稿与上一快照对比 </Button> </div> - <Status busy={false} error={snapshotErr} empty={snapshotMsg} /> + {#if snapshotErr} + <Status busy={false} error={snapshotErr} empty={null} /> + {:else if snapshotMsg} + <Status busy={false} empty={snapshotMsg} /> + {/if} {#if conflictServer} <Alert variant="default"> @@ -310,20 +321,18 @@ </span> </Alert> <DiffView before={conflictServer.code} after={code} beforeLabel="服务器" afterLabel="本机" /> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> <Button onclick={keepMine}>保留本机修改(覆盖服务器)</Button> <Button variant="outline" onclick={loadServer}>采用服务器版本</Button> </div> {/if} </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">AI 助手</h2> - <Label for="ai-instruction">希望 AI 帮你做什么?<span class="text-sm text-muted-foreground">例如:把均线周期改为两个可调参数并解释影响</span></Label> - <textarea id="ai-instruction" class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" rows="3" bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览"></textarea> - <div class="flex items-center gap-2 flex-wrap"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">AI 助手</h2> <Button class="w-fit" disabled={aiBusy || !instruction.trim()} onclick={askAI}> - {aiBusy ? 'AI 处理中…' : '获取建议'} + {aiBusy ? 'AI 处理中…' : 'AI 改进'} </Button> </div> {#if aiErr} @@ -333,20 +342,25 @@ </Alert> {/if} + <div> + <Label for="ai-instruction">希望 AI 帮你做什么?<span class="text-xs text-muted-foreground">例如:把均线周期改为两个可调参数并解释影响</span></Label> + <Textarea.Root id="ai-instruction" class="w-full" rows={3} bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览" /> + </div> + {#if aiResult} - <div class="flex flex-col gap-2"> - <div class="flex flex-wrap justify-between items-center gap-2"> - <h3 class="text-base font-semibold">建议解释 <span class="text-sm text-muted-foreground">{aiResult.model}</span></h3> - <span class="text-sm text-muted-foreground"> + <div class="flex flex-col gap-4"> + <div class="flex flex-wrap justify-between items-center gap-4"> + <h3 class="text-sm font-medium m-0">建议解释 <span class="text-xs text-muted-foreground">{aiResult.model}</span></h3> + <span class="text-xs text-muted-foreground"> tokens {aiResult.usage?.input_tokens ?? '—'} 入 / {aiResult.usage?.output_tokens ?? '—'} 出 </span> </div> <p class="m-0">{aiResult.explanation}</p> - <h3 class="text-base font-semibold">完整建议源码</h3> + <h3 class="text-sm font-medium m-0">完整建议源码</h3> <CodeEditor value={aiResult.proposed_code} onInput={() => {}} readOnly={true} label="AI 返回代码(只读)" height={280} /> - <h3 class="text-base font-semibold">与当前草稿的差异</h3> + <h3 class="text-sm font-medium m-0">与当前草稿的差异</h3> <DiffView before={code} after={aiResult.proposed_code} beforeLabel="当前草稿" afterLabel="AI 建议" /> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> <Button class="w-fit" disabled={aiAcceptBusy} @@ -357,7 +371,7 @@ </Button> <Button variant="ghost" class="w-fit" disabled={aiAcceptBusy} onclick={discardAI}>放弃建议</Button> {#if aiResult.base_generation !== baseGeneration} - <span class="text-destructive">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span> + <span class="text-destructive text-xs">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span> {/if} {#if aiAcceptErr} <Alert variant="destructive">{aiAcceptErr}</Alert> @@ -370,14 +384,20 @@ {/if} </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">版本历史</h2> - <Status busy={versionsBusy} error={versionsErr} busyText="载入版本…" empty={null} /> - {#if !versionsBusy && versions.length === 0} + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <h2 class="text-lg font-semibold m-0">版本历史</h2> + {#if versionsBusy} + <Status busy skeletonRows={3} busyText="载入版本…" empty={null} /> + {:else if versionsErr} + <Status busy={false} error={versionsErr} empty={null}> + {#snippet errorCta()} + <Button onclick={() => void refreshVersions()}>重新载入</Button> + {/snippet} + </Status> + {:else if versions.length === 0} <Status busy={false} empty="还没有版本快照" emptyHint="保存第一个快照后可以在历史中查看完整代码与恢复" /> - {/if} - {#if !versionsBusy && versions.length > 0} - <div class="overflow-x-auto rounded-lg border border-border"> + {:else} + <div class="overflow-x-auto"> <Table.Root> <Table.Header> <Table.Row> @@ -394,7 +414,7 @@ <Table.Cell>{fmtDateTime(v.created_at)}</Table.Cell> <Table.Cell> {v.message?.trim() || '(无说明)'} - <span class="text-sm text-muted-foreground ml-1">版本代 {v.hash?.slice(0, 8) || '—'}</span> + <span class="text-xs text-muted-foreground ml-1">版本代 {v.hash?.slice(0, 8) || '—'}</span> </Table.Cell> <Table.Cell class="tabular-nums">{v.hash?.slice(0, 12)}</Table.Cell> <Table.Cell><Badge variant="outline">{versionSourceLabel[v.source]}</Badge></Table.Cell> @@ -413,7 +433,7 @@ </Table.Root> </div> {/if} - <p class="text-sm text-muted-foreground">版本为不可变记录,恢复均创建新草稿,从不改写历史。获取到最新草稿修改内容请刷新页面查看。</p> + <p class="text-xs text-muted-foreground m-0">版本为不可变记录,恢复均创建新草稿,从不改写历史。</p> </Card.Root> {#if comparing} |
