@@ -18,10 +18,9 @@ import type {
1818 AiRefineResult ,
1919 AiSettingsView ,
2020 AiTaskState ,
21- BalanceView ,
22- RefineEstimateView ,
2321} from "../types" ;
2422import RefineWorkbench from "./RefineWorkbench" ;
23+ import RefineLaunchDialog from "./RefineLaunchDialog" ;
2524
2625const btn : React . CSSProperties = { padding : "5px 10px" , cursor : "pointer" , fontSize : 12 , borderRadius : 6 } ;
2726
@@ -39,15 +38,14 @@ export default function AiRefineCard({
3938} ) {
4039 const [ settings , setSettings ] = useState < AiSettingsView | null > ( null ) ;
4140 const [ phase , setPhase ] = useState < "idle" | "consent" | "confirm" | "running" | "done" | "failed" > ( "idle" ) ;
42- const [ estimate , setEstimate ] = useState < RefineEstimateView | null > ( null ) ;
43- const [ balance , setBalance ] = useState < BalanceView | null > ( null ) ;
4441 const [ progress , setProgress ] = useState < { finished : number ; total : number } | null > ( null ) ;
4542 const [ result , setResult ] = useState < AiRefineResult | null > ( null ) ;
4643 const [ failure , setFailure ] = useState < AiTaskFailureLike | null > ( null ) ;
47- const [ remember , setRemember ] = useState ( false ) ;
4844 const [ msg , setMsg ] = useState ( "" ) ;
4945 const [ , setTaskId ] = useState < number | null > ( null ) ;
5046 const [ showWorkbench , setShowWorkbench ] = useState ( false ) ;
47+ // v0.17.0:策略发起对话框(目标/档位/旋钮/提示词预览/成本确认在对话框内)
48+ const [ showLaunch , setShowLaunch ] = useState ( false ) ;
5149 // v0.16.1 审查修复:工作台「重新生成」不经用户启动路径——旁路导航跳转
5250 // (否则每次 regenerate 都会 onTaskStarted → App 把你从会话页拽到 AI 对话页)
5351 const skipNavigateRef = useRef ( false ) ;
@@ -102,7 +100,7 @@ export default function AiRefineCard({
102100 setMsg ( "任务 30 秒无进展(可能未启动或后台卡住)——请查看 tauri 终端 [refine-task] 日志后重试" ) ;
103101 } ) ;
104102
105- /** ① 预估 + 余额(确认弹窗数据源 ) */
103+ /** ① 快速检查 → 打开策略发起对话框(授权/成本确认/策略选择在对话框内 ) */
106104 const prepare = async ( ) => {
107105 setMsg ( "" ) ;
108106 if ( ! settings ?. enabled ) {
@@ -115,36 +113,22 @@ export default function AiRefineCard({
115113 setPhase ( "idle" ) ;
116114 return ;
117115 }
118- if ( ! settings . authorized ) {
119- setPhase ( "consent" ) ;
120- return ;
121- }
122- const est = await invoke < RefineEstimateView > ( "ai_refine_estimate" , { sessionId } ) . catch ( ( ) => null ) ;
123- const bal = await invoke < BalanceView > ( "ai_get_balance" ) . catch ( ( ) => null ) ;
124- if ( ! est ) {
125- setMsg ( "成本预估失败,无法继续" ) ;
126- return ;
127- }
128- setEstimate ( est ) ;
129- setBalance ( bal ) ;
130- setRemember ( est . rememberCostChoice ) ;
131- // 已"记住此选择"且同意过 → 跳过确认直接开始
132- if ( est . rememberCostChoice ) {
133- void start ( est ) ;
134- } else {
135- setPhase ( "confirm" ) ;
136- }
116+ setShowLaunch ( true ) ;
137117 } ;
138118
139- /** ② 启动任务(确认/授权通过后) */
140- const start = async ( est ?: RefineEstimateView ) => {
119+ /** ② 对话框确认启动回调(策略已由对话框传入——回接轮询/事件双通道) */
120+ const handleDialogStarted = useCallback ( ( sid : number , taskId : number ) => {
121+ setPhase ( "running" ) ;
122+ taskIdRef . current = taskId ;
123+ // v0.16.1:会话页精修启动 → 自动跳 AI 对话页(任务卡入聊天线程/可追问)
124+ if ( ! skipNavigateRef . current ) onTaskStarted ?.( sid , taskId ) ;
125+ setTaskId ( taskId ) ;
126+ startPolling ( taskId ) ;
127+ } , [ onTaskStarted , startPolling ] ) ;
128+
129+ /** ② 启动任务(工作台「重新生成」路径——沿全局默认偏好;策略选择在主发起对话框) */
130+ const start = async ( ) => {
141131 setMsg ( "" ) ;
142- if ( remember && ! est ) {
143- // 勾选"记住" → 持久化偏好
144- await invoke ( "ai_update_settings" , {
145- settings : { ...settings , rememberCostChoice : true } ,
146- } ) . catch ( ( ) => undefined ) ;
147- }
148132 setPhase ( "running" ) ;
149133 // 契约修复(2026-08-21 真机"排队中"根因):Rust AiTaskHandle 为 camelCase
150134 // 序列化(taskId),此前读 handle.task_id 恒为 undefined → 事件被忽略、
@@ -177,12 +161,8 @@ export default function AiRefineCard({
177161 setResult ( null ) ;
178162 setFailure ( null ) ;
179163 setProgress ( null ) ;
180- setEstimate ( null ) ;
181- setBalance ( null ) ;
182164 } ;
183165
184- const est = estimate ?. estimate ;
185-
186166 return (
187167 < div style = { { border : "1px solid #c7d2fe" , borderRadius : 8 , padding : 10 , marginBottom : 8 , background : "#f5f3ff" } } >
188168 < div style = { { display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 6 } } >
@@ -204,56 +184,8 @@ export default function AiRefineCard({
204184 ) }
205185 </ div >
206186
207- { /* 授权卡(首次:上传内容说明) */ }
208- { phase === "consent" && (
209- < div style = { { border : "1px solid #f59e0b" , background : "#fffbeb" , borderRadius : 6 , padding : 8 , marginBottom : 6 , fontSize : 12 } } >
210- < div style = { { fontWeight : 600 , marginBottom : 4 } } > 首次使用需授权</ div >
211- 精修将上传< strong > 转写文本与最小上下文</ strong > 至 DeepSeek;本地优先铁律:< strong > 音视频/图像永不出本机</ strong > 。是否同意?
212- < div style = { { marginTop : 6 , display : "flex" , gap : 6 } } >
213- < button style = { { ...btn , background : "#0d9488" , color : "#fff" , border : "none" } } onClick = { async ( ) => {
214- await invoke ( "ai_set_authorized" , { authorized : true } ) . catch ( ( ) => undefined ) ;
215- setSettings ( { ...settings ! , authorized : true } ) ;
216- setPhase ( "confirm" ) ;
217- } } > 同意并继续</ button >
218- < button style = { btn } onClick = { ( ) => setPhase ( "idle" ) } > 暂不</ button >
219- </ div >
220- </ div >
221- ) }
222-
223- { /* 成本确认(REQ-143 基础版:token 预估 + 费用 + 内联余额) */ }
224- { phase === "confirm" && est && (
225- < div style = { { border : "1px solid #e5e7eb" , background : "#fff" , borderRadius : 6 , padding : 8 , marginBottom : 6 , fontSize : 12 } } >
226- < div style = { { fontWeight : 600 , marginBottom : 4 } } > 成本确认</ div >
227- < div >
228- 预估 token:< strong > { est . estTokens } </ strong > · 预估费用:< strong > ¥{ est . estCostYuan . toFixed ( 4 ) } </ strong >
229- { est . pricePer1m > 0 && < span style = { { color : "#6b7280" } } > (单价 ¥{ est . pricePer1m } /1M token)</ span > }
230- { est . pricePer1m === 0 && < span style = { { color : "#0d9488" } } > (当前模型免费档 ¥0)</ span > }
231- { est . pricePer1m === 0 && est . priceKnown === false && < span style = { { color : "#d97706" } } > (该模型单价未登记,费用可能不准确)</ span > }
232- </ div >
233- { balance && (
234- < div style = { { color : balance . lowBalanceWarning ? "#dc2626" : "#374151" } } >
235- 当前余额:< strong > ¥{ balance . balance . totalBalance . toFixed ( 2 ) } </ strong >
236- { balance . lowBalanceWarning && < span style = { { marginLeft : 6 } } > ⚠️ { balance . lowBalanceWarning } </ span > }
237- </ div >
238- ) }
239- { /* F3-D:成本硬拦截提示——余额不足时启动会被后端拒绝(三出口引导) */ }
240- { balance && est . estCostYuan > 0 && balance . balance . totalBalance < est . estCostYuan * 1.2 && (
241- < div style = { { color : "#b91c1c" , marginTop : 2 } } >
242- ⚠️ 余额不足本次预估(含安全系数)——启动将被拦截,请充值或切换免费档模型
243- </ div >
244- ) }
245- < label style = { { display : "flex" , alignItems : "center" , gap : 4 , margin : "4px 0" } } >
246- < input type = "checkbox" checked = { remember } onChange = { ( e ) => setRemember ( e . target . checked ) } />
247- 记住此选择,下次不再确认
248- </ label >
249- < div style = { { display : "flex" , gap : 6 } } >
250- < button style = { { ...btn , background : "#0d9488" , color : "#fff" , border : "none" } } onClick = { ( ) => void start ( ) } >
251- 开始精修
252- </ button >
253- < button style = { btn } onClick = { reset } > 取消</ button >
254- </ div >
255- </ div >
256- ) }
187+ { /* 授权卡由 RefineLaunchDialog 承载(v0.17.0——策略/成本/授权一屏编排) */ }
188+ { phase === "consent" && null }
257189
258190 { /* 任务进行中(切片进度) */ }
259191 { phase === "running" && (
@@ -315,6 +247,15 @@ export default function AiRefineCard({
315247 ) }
316248
317249 { msg && < div style = { { fontSize : 11 , color : msg . startsWith ( "已" ) ? "#0d9488" : "#dc2626" , marginTop : 4 } } > { msg } </ div > }
250+
251+ { /* v0.17.0:策略发起对话框(目标/档位/旋钮/预览/成本/授权) */ }
252+ { showLaunch && (
253+ < RefineLaunchDialog
254+ sessionId = { sessionId }
255+ onClose = { ( ) => setShowLaunch ( false ) }
256+ onStarted = { handleDialogStarted }
257+ />
258+ ) }
318259 </ div >
319260 ) ;
320261}
0 commit comments