summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectStrategyTab.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages/ProjectStrategyTab.svelte')
-rw-r--r--frontend/src/pages/ProjectStrategyTab.svelte94
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}