App.vue 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304
  1. <template>
  2. <main class="app-shell" :class="{ expanded: isExpanded }">
  3. <div class="aurora" aria-hidden="true">
  4. <span></span>
  5. <span></span>
  6. <span></span>
  7. </div>
  8. <!-- 初始状态:居中输入卡片 -->
  9. <div v-if="!isExpanded" class="layout layout-centered">
  10. <section class="panel panel-form panel-centered">
  11. <header class="panel-head">
  12. <div class="logo">
  13. <svg viewBox="0 0 24 24" aria-hidden="true">
  14. <path
  15. d="M12 2.5c-.7 0-1.4.2-2 .6L4.6 7C3.6 7.6 3 8.7 3 9.9v4.2c0 1.2.6 2.3 1.6 2.9l5.4 3.9c1.2.8 2.8.8 4 0l5.4-3.9c1-.7 1.6-1.7 1.6-2.9V9.9c0-1.2-.6-2.3-1.6-2.9L14 3.1a3.6 3.6 0 0 0-2-.6Z"
  16. />
  17. </svg>
  18. </div>
  19. <div>
  20. <h1>深度研究助手</h1>
  21. <p>结合多轮智能检索与总结,实时呈现洞见与引用。</p>
  22. </div>
  23. </header>
  24. <form class="form" @submit.prevent="handleSubmit">
  25. <label class="field">
  26. <span>研究主题</span>
  27. <textarea
  28. v-model="form.topic"
  29. placeholder="例如:探索多模态模型在 2025 年的关键突破"
  30. rows="4"
  31. required
  32. ></textarea>
  33. </label>
  34. <section class="options">
  35. <label class="field option">
  36. <span>搜索引擎</span>
  37. <select v-model="form.searchApi">
  38. <option value="">沿用后端配置</option>
  39. <option
  40. v-for="option in searchOptions"
  41. :key="option"
  42. :value="option"
  43. >
  44. {{ option }}
  45. </option>
  46. </select>
  47. </label>
  48. </section>
  49. <div class="form-actions">
  50. <button class="submit" type="submit" :disabled="loading">
  51. <span class="submit-label">
  52. <svg
  53. v-if="loading"
  54. class="spinner"
  55. viewBox="0 0 24 24"
  56. aria-hidden="true"
  57. >
  58. <circle cx="12" cy="12" r="9" stroke-width="3" />
  59. </svg>
  60. {{ loading ? "研究进行中..." : "开始研究" }}
  61. </span>
  62. </button>
  63. <button
  64. v-if="loading"
  65. type="button"
  66. class="secondary-btn"
  67. @click="cancelResearch"
  68. >
  69. 取消研究
  70. </button>
  71. </div>
  72. </form>
  73. <p v-if="error" class="error-chip">
  74. <svg viewBox="0 0 20 20" aria-hidden="true">
  75. <path
  76. d="M10 3.2c-.3 0-.6.2-.8.5L3.4 15c-.4.7.1 1.6.8 1.6h11.6c.7 0 1.2-.9.8-1.6L10.8 3.7c-.2-.3-.5-.5-.8-.5Zm0 4.3c.4 0 .7.3.7.7v4c0 .4-.3.7-.7.7s-.7-.3-.7-.7V8.2c0-.4.3-.7.7-.7Zm0 6.6a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
  77. />
  78. </svg>
  79. {{ error }}
  80. </p>
  81. <p v-else-if="loading" class="hint muted">
  82. 正在收集线索与证据,实时进展见右侧区域。
  83. </p>
  84. </section>
  85. </div>
  86. <!-- 全屏状态:左右分栏布局 -->
  87. <div v-else class="layout layout-fullscreen">
  88. <!-- 左侧:研究信息 -->
  89. <aside class="sidebar">
  90. <div class="sidebar-header">
  91. <button class="back-btn" @click="goBack" :disabled="loading">
  92. <svg viewBox="0 0 24 24" width="20" height="20">
  93. <path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
  94. </svg>
  95. 返回
  96. </button>
  97. <h2>🔍 深度研究助手</h2>
  98. </div>
  99. <div class="research-info">
  100. <div class="info-item">
  101. <label>研究主题</label>
  102. <p class="topic-display">{{ form.topic }}</p>
  103. </div>
  104. <div class="info-item" v-if="form.searchApi">
  105. <label>搜索引擎</label>
  106. <p>{{ form.searchApi }}</p>
  107. </div>
  108. <div class="info-item" v-if="totalTasks > 0">
  109. <label>研究进度</label>
  110. <div class="progress-bar">
  111. <div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></div>
  112. </div>
  113. <p class="progress-text">{{ completedTasks }} / {{ totalTasks }} 任务完成</p>
  114. </div>
  115. </div>
  116. <div class="sidebar-actions">
  117. <button class="new-research-btn" @click="startNewResearch">
  118. <svg viewBox="0 0 24 24" width="18" height="18">
  119. <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
  120. </svg>
  121. 开始新研究
  122. </button>
  123. </div>
  124. </aside>
  125. <!-- 右侧:研究结果 -->
  126. <section
  127. class="panel panel-result"
  128. v-if="todoTasks.length || reportMarkdown || progressLogs.length"
  129. >
  130. <header class="status-bar">
  131. <div class="status-main">
  132. <div class="status-chip" :class="{ active: loading }">
  133. <span class="dot"></span>
  134. {{ loading ? "研究进行中" : "研究流程完成" }}
  135. </div>
  136. <span class="status-meta">
  137. 任务进度:{{ completedTasks }} / {{ totalTasks || todoTasks.length || 1 }}
  138. · 阶段记录 {{ progressLogs.length }} 条
  139. </span>
  140. </div>
  141. <div class="status-controls">
  142. <button class="secondary-btn" @click="logsCollapsed = !logsCollapsed">
  143. {{ logsCollapsed ? "展开流程" : "收起流程" }}
  144. </button>
  145. </div>
  146. </header>
  147. <div class="timeline-wrapper" v-show="!logsCollapsed && progressLogs.length">
  148. <transition-group name="timeline" tag="ul" class="timeline">
  149. <li v-for="(log, index) in progressLogs" :key="`${log}-${index}`">
  150. <span class="timeline-node"></span>
  151. <p>{{ log }}</p>
  152. </li>
  153. </transition-group>
  154. </div>
  155. <div class="tasks-section" v-if="todoTasks.length">
  156. <aside class="tasks-list">
  157. <h3>任务清单</h3>
  158. <ul>
  159. <li
  160. v-for="task in todoTasks"
  161. :key="task.id"
  162. :class="['task-item', { active: task.id === activeTaskId, completed: task.status === 'completed' }]"
  163. >
  164. <button
  165. type="button"
  166. class="task-button"
  167. @click="activeTaskId = task.id"
  168. >
  169. <span class="task-title">{{ task.title }}</span>
  170. <span class="task-status" :class="task.status">
  171. {{ formatTaskStatus(task.status) }}
  172. </span>
  173. </button>
  174. <p class="task-intent">{{ task.intent }}</p>
  175. </li>
  176. </ul>
  177. </aside>
  178. <article class="task-detail" v-if="currentTask">
  179. <header class="task-header">
  180. <div>
  181. <h3>{{ currentTaskTitle || "当前任务" }}</h3>
  182. <p class="muted" v-if="currentTaskIntent">
  183. {{ currentTaskIntent }}
  184. </p>
  185. </div>
  186. <div class="task-chip-group">
  187. <span class="task-label">查询:{{ currentTaskQuery || "" }}</span>
  188. <span
  189. v-if="currentTaskNoteId"
  190. class="task-label note-chip"
  191. :title="currentTaskNoteId"
  192. >
  193. 笔记:{{ currentTaskNoteId }}
  194. </span>
  195. <span
  196. v-if="currentTaskNotePath"
  197. class="task-label note-chip path-chip"
  198. :title="currentTaskNotePath"
  199. >
  200. <span class="path-label">路径:</span>
  201. <span class="path-text">{{ currentTaskNotePath }}</span>
  202. <button
  203. class="chip-action"
  204. type="button"
  205. @click="copyNotePath(currentTaskNotePath)"
  206. >
  207. 复制
  208. </button>
  209. </span>
  210. </div>
  211. </header>
  212. <section v-if="currentTask && currentTask.notices.length" class="task-notices">
  213. <h4>系统提示</h4>
  214. <ul>
  215. <li v-for="(notice, idx) in currentTask.notices" :key="`${notice}-${idx}`">
  216. {{ notice }}
  217. </li>
  218. </ul>
  219. </section>
  220. <section
  221. class="sources-block"
  222. :class="{ 'block-highlight': sourcesHighlight }"
  223. >
  224. <h3>最新来源</h3>
  225. <template v-if="currentTaskSources.length">
  226. <ul class="sources-list">
  227. <li
  228. v-for="(item, index) in currentTaskSources"
  229. :key="`${item.title}-${index}`"
  230. class="source-item"
  231. >
  232. <a
  233. class="source-link"
  234. :href="item.url || '#'"
  235. target="_blank"
  236. rel="noopener noreferrer"
  237. >
  238. {{ item.title || item.url || `来源 ${index + 1}` }}
  239. </a>
  240. <div v-if="item.snippet || item.raw" class="source-tooltip">
  241. <p v-if="item.snippet">{{ item.snippet }}</p>
  242. <p v-if="item.raw" class="muted-text">{{ item.raw }}</p>
  243. </div>
  244. </li>
  245. </ul>
  246. </template>
  247. <p v-else class="muted">暂无可用来源</p>
  248. </section>
  249. <section
  250. class="summary-block"
  251. :class="{ 'block-highlight': summaryHighlight }"
  252. >
  253. <h3>任务总结</h3>
  254. <pre class="block-pre">{{ currentTaskSummary || "暂无可用信息" }}</pre>
  255. </section>
  256. <section
  257. class="tools-block"
  258. :class="{ 'block-highlight': toolHighlight }"
  259. v-if="currentTaskToolCalls.length"
  260. >
  261. <h3>工具调用记录</h3>
  262. <ul class="tool-list">
  263. <li
  264. v-for="entry in currentTaskToolCalls"
  265. :key="`${entry.eventId}-${entry.timestamp}`"
  266. class="tool-entry"
  267. >
  268. <div class="tool-entry-header">
  269. <span class="tool-entry-title">
  270. #{{ entry.eventId }} {{ entry.agent }} → {{ entry.tool }}
  271. </span>
  272. <span
  273. v-if="entry.noteId"
  274. class="tool-entry-note"
  275. >
  276. 笔记:{{ entry.noteId }}
  277. </span>
  278. </div>
  279. <p v-if="entry.notePath" class="tool-entry-path">
  280. 笔记路径:
  281. <button
  282. class="link-btn"
  283. type="button"
  284. @click="copyNotePath(entry.notePath)"
  285. >
  286. 复制
  287. </button>
  288. <span class="path-text">{{ entry.notePath }}</span>
  289. </p>
  290. <p class="tool-subtitle">参数</p>
  291. <pre class="tool-pre">{{ formatToolParameters(entry.parameters) }}</pre>
  292. <template v-if="entry.result">
  293. <p class="tool-subtitle">执行结果</p>
  294. <pre class="tool-pre">{{ formatToolResult(entry.result) }}</pre>
  295. </template>
  296. </li>
  297. </ul>
  298. </section>
  299. </article>
  300. <article class="task-detail" v-else>
  301. <p class="muted">等待任务规划或执行结果。</p>
  302. </article>
  303. </div>
  304. <div
  305. v-if="reportMarkdown"
  306. class="report-block"
  307. :class="{ 'block-highlight': reportHighlight }"
  308. >
  309. <h3>最终报告</h3>
  310. <pre class="block-pre">{{ reportMarkdown }}</pre>
  311. </div>
  312. </section>
  313. </div>
  314. </main>
  315. </template>
  316. <script lang="ts" setup>
  317. import { computed, onBeforeUnmount, reactive, ref } from "vue";
  318. import {
  319. runResearchStream,
  320. type ResearchStreamEvent
  321. } from "./services/api";
  322. interface SourceItem {
  323. title: string;
  324. url: string;
  325. snippet: string;
  326. raw: string;
  327. }
  328. interface ToolCallLog {
  329. eventId: number;
  330. agent: string;
  331. tool: string;
  332. parameters: Record<string, unknown>;
  333. result: string;
  334. noteId: string | null;
  335. notePath: string | null;
  336. timestamp: number;
  337. }
  338. interface TodoTaskView {
  339. id: number;
  340. title: string;
  341. intent: string;
  342. query: string;
  343. status: string;
  344. summary: string;
  345. sourcesSummary: string;
  346. sourceItems: SourceItem[];
  347. notices: string[];
  348. noteId: string | null;
  349. notePath: string | null;
  350. toolCalls: ToolCallLog[];
  351. }
  352. const form = reactive({
  353. topic: "",
  354. searchApi: ""
  355. });
  356. const loading = ref(false);
  357. const error = ref("");
  358. const progressLogs = ref<string[]>([]);
  359. const logsCollapsed = ref(false);
  360. const isExpanded = ref(false);
  361. const todoTasks = ref<TodoTaskView[]>([]);
  362. const activeTaskId = ref<number | null>(null);
  363. const reportMarkdown = ref("");
  364. const summaryHighlight = ref(false);
  365. const sourcesHighlight = ref(false);
  366. const reportHighlight = ref(false);
  367. const toolHighlight = ref(false);
  368. let currentController: AbortController | null = null;
  369. const searchOptions = [
  370. "advanced",
  371. "duckduckgo",
  372. "tavily",
  373. "perplexity",
  374. "searxng"
  375. ];
  376. const TASK_STATUS_LABEL: Record<string, string> = {
  377. pending: "待执行",
  378. in_progress: "进行中",
  379. completed: "已完成",
  380. skipped: "已跳过"
  381. };
  382. function formatTaskStatus(status: string): string {
  383. return TASK_STATUS_LABEL[status] ?? status;
  384. }
  385. const totalTasks = computed(() => todoTasks.value.length);
  386. const completedTasks = computed(() =>
  387. todoTasks.value.filter((task) => task.status === "completed").length
  388. );
  389. const currentTask = computed(() => {
  390. if (activeTaskId.value !== null) {
  391. return todoTasks.value.find((task) => task.id === activeTaskId.value) ?? null;
  392. }
  393. return todoTasks.value[0] ?? null;
  394. });
  395. const currentTaskSources = computed(() => currentTask.value?.sourceItems ?? []);
  396. const currentTaskSummary = computed(() => currentTask.value?.summary ?? "");
  397. const currentTaskTitle = computed(() => currentTask.value?.title ?? "");
  398. const currentTaskIntent = computed(() => currentTask.value?.intent ?? "");
  399. const currentTaskQuery = computed(() => currentTask.value?.query ?? "");
  400. const currentTaskNoteId = computed(() => currentTask.value?.noteId ?? "");
  401. const currentTaskNotePath = computed(() => currentTask.value?.notePath ?? "");
  402. const currentTaskToolCalls = computed(
  403. () => currentTask.value?.toolCalls ?? []
  404. );
  405. const pulse = (flag: typeof summaryHighlight) => {
  406. flag.value = false;
  407. requestAnimationFrame(() => {
  408. flag.value = true;
  409. window.setTimeout(() => {
  410. flag.value = false;
  411. }, 1200);
  412. });
  413. };
  414. function parseSources(raw: string): SourceItem[] {
  415. if (!raw) {
  416. return [];
  417. }
  418. const items: SourceItem[] = [];
  419. const lines = raw.split("\n");
  420. let current: SourceItem | null = null;
  421. const truncate = (value: string, max = 360) => {
  422. const trimmed = value.trim();
  423. return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
  424. };
  425. const flush = () => {
  426. if (!current) {
  427. return;
  428. }
  429. const normalized: SourceItem = {
  430. title: current.title?.trim() || "",
  431. url: current.url?.trim() || "",
  432. snippet: current.snippet ? truncate(current.snippet) : "",
  433. raw: current.raw ? truncate(current.raw, 420) : ""
  434. };
  435. if (
  436. normalized.title ||
  437. normalized.url ||
  438. normalized.snippet ||
  439. normalized.raw
  440. ) {
  441. if (!normalized.title && normalized.url) {
  442. normalized.title = normalized.url;
  443. }
  444. items.push(normalized);
  445. }
  446. current = null;
  447. };
  448. const ensureCurrent = () => {
  449. if (!current) {
  450. current = { title: "", url: "", snippet: "", raw: "" };
  451. }
  452. };
  453. for (const line of lines) {
  454. const trimmed = line.trim();
  455. if (!trimmed) {
  456. continue;
  457. }
  458. if (/^\*/.test(trimmed) && trimmed.includes(" : ")) {
  459. flush();
  460. const withoutBullet = trimmed.replace(/^\*\s*/, "");
  461. const [titlePart, urlPart] = withoutBullet.split(" : ");
  462. current = {
  463. title: titlePart?.trim() || "",
  464. url: urlPart?.trim() || "",
  465. snippet: "",
  466. raw: ""
  467. };
  468. continue;
  469. }
  470. if (/^(Source|信息来源)\s*:/.test(trimmed)) {
  471. flush();
  472. const [, titlePart = ""] = trimmed.split(/:\s*(.+)/);
  473. current = {
  474. title: titlePart.trim(),
  475. url: "",
  476. snippet: "",
  477. raw: ""
  478. };
  479. continue;
  480. }
  481. if (/^URL\s*:/.test(trimmed)) {
  482. ensureCurrent();
  483. const [, urlPart = ""] = trimmed.split(/:\s*(.+)/);
  484. current!.url = urlPart.trim();
  485. continue;
  486. }
  487. if (
  488. /^(Most relevant content from source|信息内容)\s*:/.test(trimmed)
  489. ) {
  490. ensureCurrent();
  491. const [, contentPart = ""] = trimmed.split(/:\s*(.+)/);
  492. current!.snippet = contentPart.trim();
  493. continue;
  494. }
  495. if (
  496. /^(Full source content limited to|信息内容限制为)\s*:/.test(trimmed)
  497. ) {
  498. ensureCurrent();
  499. const [, rawPart = ""] = trimmed.split(/:\s*(.+)/);
  500. current!.raw = rawPart.trim();
  501. continue;
  502. }
  503. if (/^https?:\/\//.test(trimmed)) {
  504. ensureCurrent();
  505. if (!current!.url) {
  506. current!.url = trimmed;
  507. continue;
  508. }
  509. }
  510. ensureCurrent();
  511. current!.raw = current!.raw ? `${current!.raw}\n${trimmed}` : trimmed;
  512. }
  513. flush();
  514. return items;
  515. }
  516. function extractOptionalString(value: unknown): string | null {
  517. if (typeof value !== "string") {
  518. return null;
  519. }
  520. const trimmed = value.trim();
  521. return trimmed ? trimmed : null;
  522. }
  523. function ensureRecord(value: unknown): Record<string, unknown> {
  524. if (value && typeof value === "object" && !Array.isArray(value)) {
  525. return value as Record<string, unknown>;
  526. }
  527. return {};
  528. }
  529. function applyNoteMetadata(
  530. task: TodoTaskView,
  531. payload: Record<string, unknown>
  532. ): void {
  533. const noteId = extractOptionalString(payload.note_id);
  534. if (noteId) {
  535. task.noteId = noteId;
  536. }
  537. const notePath = extractOptionalString(payload.note_path);
  538. if (notePath) {
  539. task.notePath = notePath;
  540. }
  541. }
  542. function formatToolParameters(parameters: Record<string, unknown>): string {
  543. try {
  544. return JSON.stringify(parameters, null, 2);
  545. } catch (error) {
  546. console.warn("无法格式化工具参数", error, parameters);
  547. return Object.entries(parameters)
  548. .map(([key, value]) => `${key}: ${String(value)}`)
  549. .join("\n");
  550. }
  551. }
  552. function formatToolResult(result: string): string {
  553. const trimmed = result.trim();
  554. const limit = 900;
  555. if (trimmed.length > limit) {
  556. return `${trimmed.slice(0, limit)}…`;
  557. }
  558. return trimmed;
  559. }
  560. async function copyNotePath(path: string | null | undefined) {
  561. if (!path) {
  562. return;
  563. }
  564. try {
  565. await navigator.clipboard.writeText(path);
  566. progressLogs.value.push(`已复制笔记路径:${path}`);
  567. } catch (error) {
  568. console.warn("无法直接复制到剪贴板", error);
  569. window.prompt("复制以下笔记路径", path);
  570. progressLogs.value.push("请手动复制笔记路径");
  571. }
  572. }
  573. function resetWorkflowState() {
  574. todoTasks.value = [];
  575. activeTaskId.value = null;
  576. reportMarkdown.value = "";
  577. progressLogs.value = [];
  578. summaryHighlight.value = false;
  579. sourcesHighlight.value = false;
  580. reportHighlight.value = false;
  581. toolHighlight.value = false;
  582. logsCollapsed.value = false;
  583. }
  584. function findTask(taskId: unknown): TodoTaskView | undefined {
  585. const numeric =
  586. typeof taskId === "number"
  587. ? taskId
  588. : typeof taskId === "string"
  589. ? Number(taskId)
  590. : NaN;
  591. if (Number.isNaN(numeric)) {
  592. return undefined;
  593. }
  594. return todoTasks.value.find((task) => task.id === numeric);
  595. }
  596. function upsertTaskMetadata(task: TodoTaskView, payload: Record<string, unknown>) {
  597. if (typeof payload.title === "string" && payload.title.trim()) {
  598. task.title = payload.title.trim();
  599. }
  600. if (typeof payload.intent === "string" && payload.intent.trim()) {
  601. task.intent = payload.intent.trim();
  602. }
  603. if (typeof payload.query === "string" && payload.query.trim()) {
  604. task.query = payload.query.trim();
  605. }
  606. }
  607. const handleSubmit = async () => {
  608. if (!form.topic.trim()) {
  609. error.value = "请输入研究主题";
  610. return;
  611. }
  612. if (currentController) {
  613. currentController.abort();
  614. currentController = null;
  615. }
  616. loading.value = true;
  617. error.value = "";
  618. isExpanded.value = true;
  619. resetWorkflowState();
  620. const controller = new AbortController();
  621. currentController = controller;
  622. const payload = {
  623. topic: form.topic.trim(),
  624. search_api: form.searchApi || undefined
  625. };
  626. try {
  627. await runResearchStream(
  628. payload,
  629. (event: ResearchStreamEvent) => {
  630. if (event.type === "status") {
  631. const message =
  632. typeof event.message === "string" && event.message.trim()
  633. ? event.message
  634. : "流程状态更新";
  635. progressLogs.value.push(message);
  636. const payload = event as Record<string, unknown>;
  637. const task = findTask(payload.task_id);
  638. if (task && message) {
  639. task.notices.push(message);
  640. applyNoteMetadata(task, payload);
  641. }
  642. return;
  643. }
  644. if (event.type === "todo_list") {
  645. const tasks = Array.isArray(event.tasks)
  646. ? (event.tasks as Record<string, unknown>[])
  647. : [];
  648. todoTasks.value = tasks.map((item, index) => {
  649. const rawId =
  650. typeof item.id === "number"
  651. ? item.id
  652. : typeof item.id === "string"
  653. ? Number(item.id)
  654. : index + 1;
  655. const id = Number.isFinite(rawId) ? Number(rawId) : index + 1;
  656. const noteId =
  657. typeof item.note_id === "string" && item.note_id.trim()
  658. ? item.note_id.trim()
  659. : null;
  660. const notePath =
  661. typeof item.note_path === "string" && item.note_path.trim()
  662. ? item.note_path.trim()
  663. : null;
  664. return {
  665. id,
  666. title:
  667. typeof item.title === "string" && item.title.trim()
  668. ? item.title.trim()
  669. : `任务${id}`,
  670. intent:
  671. typeof item.intent === "string" && item.intent.trim()
  672. ? item.intent.trim()
  673. : "探索与主题相关的关键信息",
  674. query:
  675. typeof item.query === "string" && item.query.trim()
  676. ? item.query.trim()
  677. : form.topic.trim(),
  678. status:
  679. typeof item.status === "string" && item.status.trim()
  680. ? item.status.trim()
  681. : "pending",
  682. summary: "",
  683. sourcesSummary: "",
  684. sourceItems: [],
  685. notices: [],
  686. noteId,
  687. notePath,
  688. toolCalls: []
  689. } as TodoTaskView;
  690. });
  691. if (todoTasks.value.length) {
  692. activeTaskId.value = todoTasks.value[0].id;
  693. progressLogs.value.push("已生成任务清单");
  694. } else {
  695. progressLogs.value.push("未生成任务清单,使用默认任务继续");
  696. }
  697. return;
  698. }
  699. if (event.type === "task_status") {
  700. const payload = event as Record<string, unknown>;
  701. const task = findTask(event.task_id);
  702. if (!task) {
  703. return;
  704. }
  705. upsertTaskMetadata(task, payload);
  706. applyNoteMetadata(task, payload);
  707. const status =
  708. typeof event.status === "string" && event.status.trim()
  709. ? event.status.trim()
  710. : task.status;
  711. task.status = status;
  712. if (status === "in_progress") {
  713. task.summary = "";
  714. task.sourcesSummary = "";
  715. task.sourceItems = [];
  716. task.notices = [];
  717. activeTaskId.value = task.id;
  718. progressLogs.value.push(`开始执行任务:${task.title}`);
  719. } else if (status === "completed") {
  720. if (typeof event.summary === "string" && event.summary.trim()) {
  721. task.summary = event.summary.trim();
  722. }
  723. if (
  724. typeof event.sources_summary === "string" &&
  725. event.sources_summary.trim()
  726. ) {
  727. task.sourcesSummary = event.sources_summary.trim();
  728. task.sourceItems = parseSources(task.sourcesSummary);
  729. }
  730. progressLogs.value.push(`完成任务:${task.title}`);
  731. if (activeTaskId.value === task.id) {
  732. pulse(summaryHighlight);
  733. pulse(sourcesHighlight);
  734. }
  735. } else if (status === "skipped") {
  736. progressLogs.value.push(`任务跳过:${task.title}`);
  737. }
  738. return;
  739. }
  740. if (event.type === "sources") {
  741. const payload = event as Record<string, unknown>;
  742. const task = findTask(event.task_id);
  743. if (!task) {
  744. return;
  745. }
  746. const textCandidates = [
  747. payload.latest_sources,
  748. payload.sources_summary,
  749. payload.raw_context
  750. ];
  751. const latestText = textCandidates
  752. .map((value) => (typeof value === "string" ? value.trim() : ""))
  753. .find((value) => value);
  754. if (latestText) {
  755. task.sourcesSummary = latestText;
  756. task.sourceItems = parseSources(latestText);
  757. if (activeTaskId.value === task.id) {
  758. pulse(sourcesHighlight);
  759. }
  760. progressLogs.value.push(`已更新任务来源:${task.title}`);
  761. }
  762. if (typeof payload.backend === "string") {
  763. progressLogs.value.push(
  764. `当前使用搜索后端:${payload.backend}`
  765. );
  766. }
  767. applyNoteMetadata(task, payload);
  768. return;
  769. }
  770. if (event.type === "task_summary_chunk") {
  771. const payload = event as Record<string, unknown>;
  772. const task = findTask(event.task_id);
  773. if (!task) {
  774. return;
  775. }
  776. const chunk =
  777. typeof event.content === "string" ? event.content : "";
  778. task.summary += chunk;
  779. applyNoteMetadata(task, payload);
  780. if (activeTaskId.value === task.id) {
  781. pulse(summaryHighlight);
  782. }
  783. return;
  784. }
  785. if (event.type === "tool_call") {
  786. const payload = event as Record<string, unknown>;
  787. const eventId =
  788. typeof payload.event_id === "number"
  789. ? payload.event_id
  790. : Date.now();
  791. const agent =
  792. typeof payload.agent === "string" && payload.agent.trim()
  793. ? payload.agent.trim()
  794. : "Agent";
  795. const tool =
  796. typeof payload.tool === "string" && payload.tool.trim()
  797. ? payload.tool.trim()
  798. : "tool";
  799. const parameters = ensureRecord(payload.parameters);
  800. const result =
  801. typeof payload.result === "string" ? payload.result : "";
  802. const noteId = extractOptionalString(payload.note_id);
  803. const notePath = extractOptionalString(payload.note_path);
  804. const task = findTask(payload.task_id);
  805. if (task) {
  806. task.toolCalls.push({
  807. eventId,
  808. agent,
  809. tool,
  810. parameters,
  811. result,
  812. noteId,
  813. notePath,
  814. timestamp: Date.now()
  815. });
  816. if (noteId) {
  817. task.noteId = noteId;
  818. }
  819. if (notePath) {
  820. task.notePath = notePath;
  821. }
  822. const logSummary = noteId
  823. ? `${agent} 调用了 ${tool}(任务 ${task.id},笔记 ${noteId})`
  824. : `${agent} 调用了 ${tool}(任务 ${task.id})`;
  825. progressLogs.value.push(logSummary);
  826. if (activeTaskId.value === task.id) {
  827. pulse(toolHighlight);
  828. }
  829. } else {
  830. progressLogs.value.push(`${agent} 调用了 ${tool}`);
  831. }
  832. return;
  833. }
  834. if (event.type === "final_report") {
  835. const report =
  836. typeof event.report === "string" && event.report.trim()
  837. ? event.report.trim()
  838. : "";
  839. reportMarkdown.value = report || "报告生成失败,未获得有效内容";
  840. pulse(reportHighlight);
  841. progressLogs.value.push("最终报告已生成");
  842. return;
  843. }
  844. if (event.type === "error") {
  845. const detail =
  846. typeof event.detail === "string" && event.detail.trim()
  847. ? event.detail
  848. : "研究过程中发生错误";
  849. error.value = detail;
  850. progressLogs.value.push("研究失败,已停止流程");
  851. }
  852. },
  853. { signal: controller.signal }
  854. );
  855. if (!reportMarkdown.value) {
  856. reportMarkdown.value = "暂无生成的报告";
  857. }
  858. } catch (err) {
  859. if (err instanceof DOMException && err.name === "AbortError") {
  860. progressLogs.value.push("已取消当前研究任务");
  861. } else {
  862. error.value = err instanceof Error ? err.message : "请求失败";
  863. }
  864. } finally {
  865. loading.value = false;
  866. if (currentController === controller) {
  867. currentController = null;
  868. }
  869. }
  870. };
  871. const cancelResearch = () => {
  872. if (!loading.value || !currentController) {
  873. return;
  874. }
  875. progressLogs.value.push("正在尝试取消当前研究任务…");
  876. currentController.abort();
  877. };
  878. const goBack = () => {
  879. if (loading.value) {
  880. return; // 研究进行中不允许返回
  881. }
  882. isExpanded.value = false;
  883. };
  884. const startNewResearch = () => {
  885. if (loading.value) {
  886. cancelResearch();
  887. }
  888. resetWorkflowState();
  889. isExpanded.value = false;
  890. form.topic = "";
  891. form.searchApi = "";
  892. };
  893. onBeforeUnmount(() => {
  894. if (currentController) {
  895. currentController.abort();
  896. currentController = null;
  897. }
  898. });
  899. </script>
  900. <style scoped>
  901. .app-shell {
  902. position: relative;
  903. min-height: 100vh;
  904. padding: 72px 24px;
  905. display: flex;
  906. justify-content: center;
  907. align-items: center;
  908. background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
  909. color: #1f2937;
  910. overflow: hidden;
  911. box-sizing: border-box;
  912. transition: padding 0.4s ease;
  913. }
  914. .app-shell.expanded {
  915. padding: 0;
  916. align-items: stretch;
  917. }
  918. .aurora {
  919. position: absolute;
  920. inset: 0;
  921. pointer-events: none;
  922. opacity: 0.55;
  923. }
  924. .aurora span {
  925. position: absolute;
  926. width: 45vw;
  927. height: 45vw;
  928. max-width: 520px;
  929. max-height: 520px;
  930. background: radial-gradient(circle, rgba(148, 197, 255, 0.35), transparent 60%);
  931. filter: blur(90px);
  932. animation: float 26s infinite linear;
  933. }
  934. .aurora span:nth-child(1) {
  935. top: -20%;
  936. left: -18%;
  937. animation-delay: 0s;
  938. }
  939. .aurora span:nth-child(2) {
  940. bottom: -25%;
  941. right: -20%;
  942. background: radial-gradient(circle, rgba(166, 139, 255, 0.28), transparent 60%);
  943. animation-delay: -9s;
  944. }
  945. .aurora span:nth-child(3) {
  946. top: 35%;
  947. left: 45%;
  948. background: radial-gradient(circle, rgba(164, 219, 216, 0.26), transparent 60%);
  949. animation-delay: -16s;
  950. }
  951. .layout {
  952. position: relative;
  953. width: 100%;
  954. display: flex;
  955. gap: 24px;
  956. z-index: 1;
  957. transition: all 0.4s ease;
  958. }
  959. .layout-centered {
  960. max-width: 600px;
  961. justify-content: center;
  962. align-items: center;
  963. }
  964. .layout-fullscreen {
  965. height: 100vh;
  966. max-width: 100%;
  967. gap: 0;
  968. align-items: stretch;
  969. }
  970. .panel {
  971. position: relative;
  972. flex: 1 1 360px;
  973. padding: 24px;
  974. border-radius: 20px;
  975. background: rgba(255, 255, 255, 0.95);
  976. border: 1px solid rgba(148, 163, 184, 0.18);
  977. box-shadow: 0 24px 48px rgba(15, 23, 42, 0.12);
  978. backdrop-filter: blur(8px);
  979. overflow: hidden;
  980. }
  981. .panel-form {
  982. max-width: 420px;
  983. }
  984. .panel-centered {
  985. width: 100%;
  986. max-width: 600px;
  987. padding: 40px;
  988. box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
  989. transform: scale(1);
  990. transition: transform 0.3s ease, box-shadow 0.3s ease;
  991. }
  992. .panel-centered:hover {
  993. transform: scale(1.02);
  994. box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
  995. }
  996. .panel-result {
  997. min-width: 360px;
  998. flex: 2 1 420px;
  999. }
  1000. .panel::before {
  1001. content: "";
  1002. position: absolute;
  1003. inset: 0;
  1004. background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(125, 86, 255, 0.1));
  1005. opacity: 0;
  1006. transition: opacity 0.35s ease;
  1007. z-index: 0;
  1008. }
  1009. .panel:hover::before {
  1010. opacity: 1;
  1011. }
  1012. .panel > * {
  1013. position: relative;
  1014. z-index: 1;
  1015. }
  1016. .panel-form h1 {
  1017. margin: 0;
  1018. font-size: 26px;
  1019. letter-spacing: 0.01em;
  1020. }
  1021. .panel-form p {
  1022. margin: 4px 0 0;
  1023. color: #64748b;
  1024. font-size: 13px;
  1025. }
  1026. .panel-head {
  1027. display: flex;
  1028. align-items: center;
  1029. gap: 16px;
  1030. margin-bottom: 24px;
  1031. }
  1032. .logo {
  1033. width: 52px;
  1034. height: 52px;
  1035. display: grid;
  1036. place-items: center;
  1037. border-radius: 16px;
  1038. background: linear-gradient(135deg, #2563eb, #7c3aed);
  1039. box-shadow: 0 12px 28px rgba(59, 130, 246, 0.4);
  1040. }
  1041. .logo svg {
  1042. width: 28px;
  1043. height: 28px;
  1044. fill: #f8fafc;
  1045. }
  1046. .form {
  1047. display: flex;
  1048. flex-direction: column;
  1049. gap: 18px;
  1050. }
  1051. .field {
  1052. display: flex;
  1053. flex-direction: column;
  1054. gap: 10px;
  1055. }
  1056. .field span {
  1057. font-weight: 600;
  1058. color: #475569;
  1059. }
  1060. textarea,
  1061. input,
  1062. select {
  1063. padding: 14px 16px;
  1064. border-radius: 16px;
  1065. border: 1px solid rgba(148, 163, 184, 0.35);
  1066. background: rgba(255, 255, 255, 0.92);
  1067. color: #1f2937;
  1068. font-size: 14px;
  1069. transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  1070. }
  1071. textarea:focus,
  1072. input:focus,
  1073. select:focus {
  1074. outline: none;
  1075. border-color: rgba(37, 99, 235, 0.65);
  1076. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
  1077. background: #ffffff;
  1078. }
  1079. .options {
  1080. display: flex;
  1081. gap: 16px;
  1082. flex-wrap: wrap;
  1083. }
  1084. .option {
  1085. flex: 1;
  1086. min-width: 140px;
  1087. }
  1088. .form-actions {
  1089. display: flex;
  1090. align-items: center;
  1091. gap: 12px;
  1092. flex-wrap: wrap;
  1093. }
  1094. .submit {
  1095. align-self: flex-start;
  1096. padding: 12px 24px;
  1097. border-radius: 16px;
  1098. border: none;
  1099. background: linear-gradient(135deg, #2563eb, #7c3aed);
  1100. color: #ffffff;
  1101. font-size: 15px;
  1102. font-weight: 600;
  1103. cursor: pointer;
  1104. transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
  1105. display: inline-flex;
  1106. align-items: center;
  1107. gap: 10px;
  1108. position: relative;
  1109. }
  1110. .submit-label {
  1111. display: inline-flex;
  1112. align-items: center;
  1113. gap: 10px;
  1114. }
  1115. .submit .spinner {
  1116. width: 18px;
  1117. height: 18px;
  1118. fill: none;
  1119. stroke: rgba(255, 255, 255, 0.85);
  1120. stroke-linecap: round;
  1121. animation: spin 1s linear infinite;
  1122. }
  1123. .submit:disabled {
  1124. opacity: 0.7;
  1125. cursor: not-allowed;
  1126. }
  1127. .submit:not(:disabled):hover {
  1128. transform: translateY(-2px);
  1129. box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
  1130. }
  1131. .secondary-btn {
  1132. padding: 10px 18px;
  1133. border-radius: 14px;
  1134. background: rgba(148, 163, 184, 0.12);
  1135. border: 1px solid rgba(148, 163, 184, 0.28);
  1136. color: #1f2937;
  1137. font-size: 14px;
  1138. font-weight: 500;
  1139. cursor: pointer;
  1140. transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
  1141. }
  1142. .secondary-btn:hover {
  1143. background: rgba(148, 163, 184, 0.2);
  1144. border-color: rgba(148, 163, 184, 0.35);
  1145. color: #0f172a;
  1146. }
  1147. .error-chip {
  1148. margin-top: 16px;
  1149. display: inline-flex;
  1150. align-items: center;
  1151. gap: 8px;
  1152. padding: 10px 14px;
  1153. background: rgba(248, 113, 113, 0.12);
  1154. border: 1px solid rgba(248, 113, 113, 0.35);
  1155. border-radius: 14px;
  1156. color: #b91c1c;
  1157. font-size: 14px;
  1158. }
  1159. .error-chip svg {
  1160. width: 18px;
  1161. height: 18px;
  1162. fill: currentColor;
  1163. }
  1164. .panel-result {
  1165. display: flex;
  1166. flex-direction: column;
  1167. gap: 18px;
  1168. }
  1169. .status-bar {
  1170. display: flex;
  1171. align-items: center;
  1172. justify-content: space-between;
  1173. gap: 12px;
  1174. flex-wrap: wrap;
  1175. }
  1176. .status-main {
  1177. display: flex;
  1178. align-items: center;
  1179. gap: 12px;
  1180. flex-wrap: wrap;
  1181. }
  1182. .status-controls {
  1183. display: flex;
  1184. gap: 8px;
  1185. }
  1186. .status-chip {
  1187. display: inline-flex;
  1188. align-items: center;
  1189. gap: 8px;
  1190. background: rgba(191, 219, 254, 0.28);
  1191. padding: 8px 14px;
  1192. border-radius: 999px;
  1193. font-size: 13px;
  1194. color: #1f2937;
  1195. border: 1px solid rgba(59, 130, 246, 0.35);
  1196. transition: background 0.3s ease, color 0.3s ease;
  1197. }
  1198. .status-chip.active {
  1199. background: rgba(129, 140, 248, 0.2);
  1200. border-color: rgba(129, 140, 248, 0.4);
  1201. color: #1e293b;
  1202. }
  1203. .status-chip .dot {
  1204. width: 8px;
  1205. height: 8px;
  1206. border-radius: 999px;
  1207. background: #2563eb;
  1208. box-shadow: 0 0 12px rgba(37, 99, 235, 0.45);
  1209. animation: pulse 1.8s ease-in-out infinite;
  1210. }
  1211. .status-meta {
  1212. color: #64748b;
  1213. font-size: 13px;
  1214. }
  1215. .timeline-wrapper {
  1216. margin-top: 12px;
  1217. max-height: 220px;
  1218. overflow-y: auto;
  1219. padding-right: 8px;
  1220. scrollbar-width: thin;
  1221. scrollbar-color: rgba(129, 140, 248, 0.45) rgba(226, 232, 240, 0.6);
  1222. }
  1223. .timeline-wrapper::-webkit-scrollbar {
  1224. width: 6px;
  1225. }
  1226. .timeline-wrapper::-webkit-scrollbar-track {
  1227. background: rgba(226, 232, 240, 0.6);
  1228. border-radius: 999px;
  1229. }
  1230. .timeline-wrapper::-webkit-scrollbar-thumb {
  1231. background: linear-gradient(180deg, rgba(129, 140, 248, 0.8), rgba(59, 130, 246, 0.7));
  1232. border-radius: 999px;
  1233. }
  1234. .timeline-wrapper::-webkit-scrollbar-thumb:hover {
  1235. background: linear-gradient(180deg, rgba(99, 102, 241, 0.9), rgba(37, 99, 235, 0.8));
  1236. }
  1237. .timeline {
  1238. list-style: none;
  1239. padding: 0;
  1240. margin: 0;
  1241. display: flex;
  1242. flex-direction: column;
  1243. gap: 14px;
  1244. position: relative;
  1245. padding-left: 12px;
  1246. }
  1247. .timeline::before {
  1248. content: "";
  1249. position: absolute;
  1250. top: 8px;
  1251. bottom: 8px;
  1252. left: 0;
  1253. width: 2px;
  1254. background: linear-gradient(180deg, rgba(59, 130, 246, 0.35), rgba(129, 140, 248, 0.15));
  1255. }
  1256. .timeline li {
  1257. position: relative;
  1258. padding-left: 24px;
  1259. color: #1e293b;
  1260. font-size: 14px;
  1261. line-height: 1.5;
  1262. }
  1263. .timeline-node {
  1264. position: absolute;
  1265. left: -12px;
  1266. top: 6px;
  1267. width: 10px;
  1268. height: 10px;
  1269. border-radius: 999px;
  1270. background: linear-gradient(135deg, #38bdf8, #7c3aed);
  1271. box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
  1272. }
  1273. .timeline-enter-active,
  1274. .timeline-leave-active {
  1275. transition: all 0.35s ease, opacity 0.35s ease;
  1276. }
  1277. .timeline-enter-from,
  1278. .timeline-leave-to {
  1279. opacity: 0;
  1280. transform: translateY(-6px);
  1281. }
  1282. .tasks-section {
  1283. display: grid;
  1284. grid-template-columns: 280px 1fr;
  1285. gap: 20px;
  1286. align-items: start;
  1287. }
  1288. @media (max-width: 960px) {
  1289. .tasks-section {
  1290. grid-template-columns: 1fr;
  1291. }
  1292. }
  1293. .tasks-list {
  1294. background: rgba(255, 255, 255, 0.92);
  1295. border: 1px solid rgba(148, 163, 184, 0.26);
  1296. border-radius: 18px;
  1297. padding: 18px;
  1298. display: flex;
  1299. flex-direction: column;
  1300. gap: 16px;
  1301. box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
  1302. }
  1303. .tasks-list h3 {
  1304. margin: 0;
  1305. font-size: 16px;
  1306. font-weight: 600;
  1307. color: #1f2937;
  1308. }
  1309. .tasks-list ul {
  1310. list-style: none;
  1311. margin: 0;
  1312. padding: 0;
  1313. display: flex;
  1314. flex-direction: column;
  1315. gap: 12px;
  1316. }
  1317. .task-item {
  1318. border-radius: 14px;
  1319. border: 1px solid transparent;
  1320. transition: border-color 0.2s ease, background 0.2s ease;
  1321. }
  1322. .task-item.completed {
  1323. border-color: rgba(56, 189, 248, 0.35);
  1324. background: rgba(191, 219, 254, 0.28);
  1325. }
  1326. .task-item.active {
  1327. border-color: rgba(129, 140, 248, 0.5);
  1328. background: rgba(224, 231, 255, 0.5);
  1329. }
  1330. .task-button {
  1331. width: 100%;
  1332. display: flex;
  1333. align-items: center;
  1334. justify-content: space-between;
  1335. gap: 12px;
  1336. padding: 12px 14px 6px;
  1337. background: transparent;
  1338. border: none;
  1339. color: inherit;
  1340. cursor: pointer;
  1341. text-align: left;
  1342. }
  1343. .task-title {
  1344. font-weight: 600;
  1345. font-size: 14px;
  1346. color: #1e293b;
  1347. }
  1348. .task-status {
  1349. display: inline-flex;
  1350. align-items: center;
  1351. justify-content: center;
  1352. padding: 4px 10px;
  1353. border-radius: 999px;
  1354. font-size: 12px;
  1355. font-weight: 500;
  1356. color: #1f2937;
  1357. background: rgba(148, 163, 184, 0.2);
  1358. }
  1359. .task-status.pending {
  1360. background: rgba(148, 163, 184, 0.18);
  1361. color: #475569;
  1362. }
  1363. .task-status.in_progress {
  1364. background: rgba(129, 140, 248, 0.24);
  1365. color: #312e81;
  1366. }
  1367. .task-status.completed {
  1368. background: rgba(34, 197, 94, 0.2);
  1369. color: #15803d;
  1370. }
  1371. .task-status.skipped {
  1372. background: rgba(248, 113, 113, 0.18);
  1373. color: #b91c1c;
  1374. }
  1375. .task-intent {
  1376. margin: 0;
  1377. padding: 0 14px 12px 14px;
  1378. font-size: 13px;
  1379. color: #64748b;
  1380. }
  1381. .task-detail {
  1382. background: rgba(255, 255, 255, 0.94);
  1383. border: 1px solid rgba(148, 163, 184, 0.26);
  1384. border-radius: 18px;
  1385. padding: 22px;
  1386. display: flex;
  1387. flex-direction: column;
  1388. gap: 18px;
  1389. box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.5);
  1390. }
  1391. .task-header {
  1392. display: flex;
  1393. justify-content: space-between;
  1394. align-items: flex-start;
  1395. flex-wrap: wrap;
  1396. gap: 12px;
  1397. }
  1398. .task-chip-group {
  1399. display: flex;
  1400. align-items: center;
  1401. gap: 8px;
  1402. flex-wrap: wrap;
  1403. }
  1404. .task-header h3 {
  1405. margin: 0;
  1406. font-size: 18px;
  1407. font-weight: 600;
  1408. color: #1f2937;
  1409. }
  1410. .task-header .muted {
  1411. margin: 6px 0 0;
  1412. }
  1413. .task-label {
  1414. padding: 6px 12px;
  1415. border-radius: 999px;
  1416. background: rgba(191, 219, 254, 0.32);
  1417. border: 1px solid rgba(59, 130, 246, 0.35);
  1418. font-size: 12px;
  1419. color: #1e3a8a;
  1420. }
  1421. .task-label.note-chip {
  1422. background: rgba(34, 197, 94, 0.2);
  1423. border-color: rgba(34, 197, 94, 0.35);
  1424. color: #15803d;
  1425. }
  1426. .task-label.path-chip {
  1427. display: inline-flex;
  1428. align-items: center;
  1429. gap: 6px;
  1430. max-width: 360px;
  1431. background: rgba(56, 189, 248, 0.2);
  1432. border-color: rgba(56, 189, 248, 0.35);
  1433. color: #0369a1;
  1434. overflow: hidden;
  1435. text-overflow: ellipsis;
  1436. white-space: nowrap;
  1437. }
  1438. .path-label {
  1439. font-weight: 500;
  1440. }
  1441. .path-text {
  1442. max-width: 220px;
  1443. overflow: hidden;
  1444. text-overflow: ellipsis;
  1445. white-space: nowrap;
  1446. }
  1447. .chip-action {
  1448. border: none;
  1449. background: rgba(56, 189, 248, 0.2);
  1450. color: #0369a1;
  1451. padding: 3px 8px;
  1452. border-radius: 10px;
  1453. font-size: 11px;
  1454. cursor: pointer;
  1455. transition: background 0.2s ease, color 0.2s ease;
  1456. }
  1457. .chip-action:hover {
  1458. background: rgba(14, 165, 233, 0.28);
  1459. color: #0f172a;
  1460. }
  1461. .task-notices {
  1462. background: rgba(191, 219, 254, 0.28);
  1463. border: 1px solid rgba(96, 165, 250, 0.35);
  1464. border-radius: 16px;
  1465. padding: 14px 18px;
  1466. color: #1f2937;
  1467. }
  1468. .task-notices h4 {
  1469. margin: 0 0 8px;
  1470. font-size: 14px;
  1471. font-weight: 600;
  1472. }
  1473. .task-notices ul {
  1474. list-style: disc;
  1475. margin: 0 0 0 18px;
  1476. padding: 0;
  1477. display: flex;
  1478. flex-direction: column;
  1479. gap: 6px;
  1480. }
  1481. .task-notices li {
  1482. font-size: 13px;
  1483. }
  1484. .report-block {
  1485. background: rgba(255, 255, 255, 0.94);
  1486. border: 1px solid rgba(148, 163, 184, 0.26);
  1487. border-radius: 18px;
  1488. padding: 22px;
  1489. display: flex;
  1490. flex-direction: column;
  1491. gap: 12px;
  1492. }
  1493. .report-block h3 {
  1494. margin: 0;
  1495. font-size: 18px;
  1496. font-weight: 600;
  1497. color: #1f2937;
  1498. }
  1499. .block-pre {
  1500. font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
  1501. Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  1502. font-size: 13px;
  1503. line-height: 1.7;
  1504. white-space: pre-wrap;
  1505. word-break: break-word;
  1506. color: #1f2937;
  1507. background: rgba(248, 250, 252, 0.9);
  1508. padding: 16px;
  1509. border-radius: 14px;
  1510. border: 1px solid rgba(148, 163, 184, 0.35);
  1511. overflow: auto;
  1512. max-height: 420px;
  1513. scrollbar-width: thin;
  1514. scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
  1515. }
  1516. .block-pre::-webkit-scrollbar {
  1517. width: 6px;
  1518. }
  1519. .block-pre::-webkit-scrollbar-track {
  1520. background: rgba(226, 232, 240, 0.7);
  1521. border-radius: 999px;
  1522. }
  1523. .block-pre::-webkit-scrollbar-thumb {
  1524. background: linear-gradient(180deg, rgba(99, 102, 241, 0.75), rgba(59, 130, 246, 0.65));
  1525. border-radius: 999px;
  1526. }
  1527. .block-pre::-webkit-scrollbar-thumb:hover {
  1528. background: linear-gradient(180deg, rgba(79, 70, 229, 0.8), rgba(37, 99, 235, 0.75));
  1529. }
  1530. .summary-block .block-pre,
  1531. .sources-block .block-pre {
  1532. max-height: 360px;
  1533. }
  1534. .tools-block {
  1535. position: relative;
  1536. margin-top: 16px;
  1537. padding: 20px;
  1538. border-radius: 18px;
  1539. background: rgba(255, 255, 255, 0.94);
  1540. border: 1px solid rgba(148, 163, 184, 0.18);
  1541. box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
  1542. display: flex;
  1543. flex-direction: column;
  1544. gap: 12px;
  1545. }
  1546. .tools-block h3 {
  1547. margin: 0;
  1548. font-size: 16px;
  1549. font-weight: 600;
  1550. color: #1f2937;
  1551. letter-spacing: 0.02em;
  1552. }
  1553. .tool-list {
  1554. list-style: none;
  1555. margin: 0;
  1556. padding: 0;
  1557. display: flex;
  1558. flex-direction: column;
  1559. gap: 12px;
  1560. }
  1561. .tool-entry {
  1562. background: rgba(248, 250, 252, 0.95);
  1563. border: 1px solid rgba(148, 163, 184, 0.24);
  1564. border-radius: 14px;
  1565. padding: 14px;
  1566. display: flex;
  1567. flex-direction: column;
  1568. gap: 10px;
  1569. }
  1570. .tool-entry-header {
  1571. display: flex;
  1572. flex-wrap: wrap;
  1573. gap: 8px;
  1574. align-items: center;
  1575. justify-content: space-between;
  1576. }
  1577. .tool-entry-title {
  1578. font-weight: 600;
  1579. color: #1f2937;
  1580. }
  1581. .tool-entry-note {
  1582. font-size: 12px;
  1583. color: #0f766e;
  1584. }
  1585. .tool-entry-path {
  1586. margin: 0;
  1587. font-size: 12px;
  1588. display: flex;
  1589. align-items: center;
  1590. gap: 6px;
  1591. color: #2563eb;
  1592. }
  1593. .tool-subtitle {
  1594. margin: 0;
  1595. font-size: 13px;
  1596. color: #475569;
  1597. font-weight: 500;
  1598. }
  1599. .tool-pre {
  1600. font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
  1601. Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  1602. font-size: 12px;
  1603. line-height: 1.6;
  1604. white-space: pre-wrap;
  1605. word-break: break-word;
  1606. color: #1f2937;
  1607. background: rgba(248, 250, 252, 0.9);
  1608. padding: 12px;
  1609. border-radius: 12px;
  1610. border: 1px solid rgba(148, 163, 184, 0.28);
  1611. overflow: auto;
  1612. max-height: 260px;
  1613. scrollbar-width: thin;
  1614. scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
  1615. }
  1616. .tool-pre::-webkit-scrollbar {
  1617. width: 6px;
  1618. }
  1619. .tool-pre::-webkit-scrollbar-track {
  1620. background: rgba(226, 232, 240, 0.7);
  1621. }
  1622. .tool-pre::-webkit-scrollbar-thumb {
  1623. background: rgba(99, 102, 241, 0.7);
  1624. border-radius: 10px;
  1625. }
  1626. .link-btn {
  1627. background: none;
  1628. border: none;
  1629. color: #0369a1;
  1630. cursor: pointer;
  1631. padding: 0 4px;
  1632. font-size: 12px;
  1633. border-radius: 8px;
  1634. transition: color 0.2s ease, background 0.2s ease;
  1635. }
  1636. .link-btn:hover {
  1637. color: #0ea5e9;
  1638. background: rgba(14, 165, 233, 0.16);
  1639. }
  1640. .sources-block,
  1641. .summary-block {
  1642. position: relative;
  1643. margin-top: 16px;
  1644. padding: 18px;
  1645. border-radius: 18px;
  1646. background: rgba(255, 255, 255, 0.94);
  1647. border: 1px solid rgba(148, 163, 184, 0.18);
  1648. box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
  1649. }
  1650. .sources-history {
  1651. margin-top: 16px;
  1652. display: flex;
  1653. flex-direction: column;
  1654. gap: 10px;
  1655. }
  1656. .sources-history h4 {
  1657. margin: 0;
  1658. color: #1f2937;
  1659. font-size: 14px;
  1660. letter-spacing: 0.01em;
  1661. }
  1662. .history-list {
  1663. display: flex;
  1664. flex-direction: column;
  1665. gap: 10px;
  1666. }
  1667. .history-list details {
  1668. background: rgba(248, 250, 252, 0.95);
  1669. border: 1px solid rgba(148, 163, 184, 0.24);
  1670. border-radius: 14px;
  1671. padding: 12px 16px;
  1672. color: #1f2937;
  1673. transition: border-color 0.2s ease, background 0.2s ease;
  1674. }
  1675. .history-list details[open] {
  1676. background: rgba(224, 231, 255, 0.55);
  1677. border-color: rgba(129, 140, 248, 0.4);
  1678. }
  1679. .history-list summary {
  1680. cursor: pointer;
  1681. font-weight: 600;
  1682. outline: none;
  1683. list-style: none;
  1684. display: flex;
  1685. align-items: center;
  1686. justify-content: space-between;
  1687. }
  1688. .history-list summary::-webkit-details-marker {
  1689. display: none;
  1690. }
  1691. .history-list summary::after {
  1692. content: "▾";
  1693. margin-left: 6px;
  1694. font-size: 12px;
  1695. opacity: 0.7;
  1696. transition: transform 0.2s ease;
  1697. }
  1698. .history-list details[open] summary::after {
  1699. transform: rotate(180deg);
  1700. }
  1701. .block-highlight {
  1702. animation: glow 1.2s ease;
  1703. }
  1704. .sources-block h3,
  1705. .summary-block h3 {
  1706. margin: 0 0 14px;
  1707. color: #1f2937;
  1708. letter-spacing: 0.02em;
  1709. }
  1710. .sources-list {
  1711. list-style: none;
  1712. margin: 0;
  1713. padding: 0;
  1714. display: flex;
  1715. flex-direction: column;
  1716. gap: 10px;
  1717. }
  1718. .source-item {
  1719. position: relative;
  1720. display: inline-flex;
  1721. flex-direction: column;
  1722. gap: 6px;
  1723. }
  1724. .source-link {
  1725. color: #2563eb;
  1726. text-decoration: none;
  1727. font-weight: 600;
  1728. letter-spacing: 0.01em;
  1729. transition: color 0.2s ease;
  1730. }
  1731. .source-link::after {
  1732. content: " ↗";
  1733. font-size: 12px;
  1734. opacity: 0.6;
  1735. }
  1736. .source-link:hover {
  1737. color: #0f172a;
  1738. }
  1739. .source-tooltip {
  1740. display: none;
  1741. position: absolute;
  1742. bottom: calc(100% + 12px);
  1743. left: 50%;
  1744. transform: translateX(-50%);
  1745. background: rgba(255, 255, 255, 0.98);
  1746. color: #1f2937;
  1747. padding: 14px 16px;
  1748. border-radius: 16px;
  1749. box-shadow: 0 18px 32px rgba(15, 23, 42, 0.18);
  1750. width: min(420px, 90vw);
  1751. z-index: 20;
  1752. border: 1px solid rgba(148, 163, 184, 0.24);
  1753. }
  1754. .source-tooltip::after {
  1755. content: "";
  1756. position: absolute;
  1757. top: 100%;
  1758. left: 50%;
  1759. transform: translateX(-50%);
  1760. border-width: 10px;
  1761. border-style: solid;
  1762. border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
  1763. }
  1764. .source-tooltip::before {
  1765. content: "";
  1766. position: absolute;
  1767. bottom: -12px;
  1768. left: 50%;
  1769. transform: translateX(-50%);
  1770. border-width: 12px 10px 0 10px;
  1771. border-style: solid;
  1772. border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
  1773. filter: drop-shadow(0 -2px 4px rgba(15, 23, 42, 0.12));
  1774. }
  1775. .source-tooltip p {
  1776. margin: 0 0 8px;
  1777. font-size: 13px;
  1778. line-height: 1.6;
  1779. }
  1780. .source-tooltip p:last-child {
  1781. margin-bottom: 0;
  1782. }
  1783. .muted-text {
  1784. color: #64748b;
  1785. }
  1786. .source-item:hover .source-tooltip,
  1787. .source-item:focus-within .source-tooltip {
  1788. display: block;
  1789. }
  1790. .hint.muted {
  1791. color: #64748b;
  1792. }
  1793. @keyframes float {
  1794. 0% {
  1795. transform: translate3d(0, 0, 0) rotate(0deg);
  1796. }
  1797. 50% {
  1798. transform: translate3d(10%, 6%, 0) rotate(3deg);
  1799. }
  1800. 100% {
  1801. transform: translate3d(0, 0, 0) rotate(0deg);
  1802. }
  1803. }
  1804. @keyframes spin {
  1805. to {
  1806. transform: rotate(360deg);
  1807. }
  1808. }
  1809. @keyframes pulse {
  1810. 0%,
  1811. 100% {
  1812. transform: scale(1);
  1813. opacity: 1;
  1814. }
  1815. 50% {
  1816. transform: scale(1.3);
  1817. opacity: 0.5;
  1818. }
  1819. }
  1820. @keyframes glow {
  1821. 0% {
  1822. box-shadow: 0 0 0 rgba(59, 130, 246, 0.3);
  1823. border-color: rgba(59, 130, 246, 0.5);
  1824. }
  1825. 100% {
  1826. box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.12);
  1827. border-color: rgba(148, 163, 184, 0.2);
  1828. }
  1829. }
  1830. @media (max-width: 960px) {
  1831. .app-shell {
  1832. padding: 56px 16px;
  1833. }
  1834. .layout {
  1835. flex-direction: column;
  1836. align-items: stretch;
  1837. }
  1838. .panel {
  1839. padding: 22px;
  1840. }
  1841. .panel-form,
  1842. .panel-result {
  1843. max-width: none;
  1844. }
  1845. .status-bar {
  1846. flex-direction: column;
  1847. align-items: flex-start;
  1848. }
  1849. .status-main,
  1850. .status-controls {
  1851. width: 100%;
  1852. }
  1853. .status-controls {
  1854. justify-content: flex-start;
  1855. }
  1856. }
  1857. @media (max-width: 600px) {
  1858. .options {
  1859. flex-direction: column;
  1860. }
  1861. .status-meta {
  1862. font-size: 12px;
  1863. }
  1864. .panel-head {
  1865. flex-direction: column;
  1866. align-items: flex-start;
  1867. }
  1868. .panel-form h1 {
  1869. font-size: 24px;
  1870. }
  1871. }
  1872. /* 侧边栏样式 */
  1873. .sidebar {
  1874. width: 400px;
  1875. min-width: 400px;
  1876. height: 100vh;
  1877. background: rgba(255, 255, 255, 0.98);
  1878. border-right: 1px solid rgba(148, 163, 184, 0.2);
  1879. padding: 32px 24px;
  1880. display: flex;
  1881. flex-direction: column;
  1882. gap: 24px;
  1883. overflow-y: auto;
  1884. box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
  1885. }
  1886. .sidebar-header {
  1887. display: flex;
  1888. flex-direction: column;
  1889. gap: 16px;
  1890. }
  1891. .sidebar-header h2 {
  1892. font-size: 24px;
  1893. font-weight: 700;
  1894. margin: 0;
  1895. color: #1f2937;
  1896. }
  1897. .back-btn {
  1898. display: flex;
  1899. align-items: center;
  1900. gap: 8px;
  1901. padding: 10px 16px;
  1902. background: transparent;
  1903. border: 1px solid rgba(148, 163, 184, 0.3);
  1904. border-radius: 12px;
  1905. color: #64748b;
  1906. font-size: 14px;
  1907. font-weight: 500;
  1908. cursor: pointer;
  1909. transition: all 0.2s ease;
  1910. width: fit-content;
  1911. }
  1912. .back-btn:hover:not(:disabled) {
  1913. background: rgba(59, 130, 246, 0.1);
  1914. border-color: #3b82f6;
  1915. color: #3b82f6;
  1916. }
  1917. .back-btn:disabled {
  1918. opacity: 0.5;
  1919. cursor: not-allowed;
  1920. }
  1921. .research-info {
  1922. flex: 1;
  1923. display: flex;
  1924. flex-direction: column;
  1925. gap: 20px;
  1926. }
  1927. .info-item {
  1928. display: flex;
  1929. flex-direction: column;
  1930. gap: 8px;
  1931. }
  1932. .info-item label {
  1933. font-size: 12px;
  1934. font-weight: 600;
  1935. text-transform: uppercase;
  1936. letter-spacing: 0.5px;
  1937. color: #64748b;
  1938. }
  1939. .info-item p {
  1940. margin: 0;
  1941. font-size: 14px;
  1942. color: #1f2937;
  1943. line-height: 1.6;
  1944. }
  1945. .topic-display {
  1946. font-size: 16px !important;
  1947. font-weight: 600;
  1948. color: #0f172a !important;
  1949. padding: 12px;
  1950. background: rgba(59, 130, 246, 0.05);
  1951. border-radius: 8px;
  1952. border-left: 3px solid #3b82f6;
  1953. }
  1954. .progress-bar {
  1955. width: 100%;
  1956. height: 8px;
  1957. background: rgba(148, 163, 184, 0.2);
  1958. border-radius: 4px;
  1959. overflow: hidden;
  1960. }
  1961. .progress-fill {
  1962. height: 100%;
  1963. background: linear-gradient(90deg, #3b82f6, #8b5cf6);
  1964. border-radius: 4px;
  1965. transition: width 0.5s ease;
  1966. }
  1967. .progress-text {
  1968. font-size: 13px !important;
  1969. color: #64748b !important;
  1970. font-weight: 500;
  1971. }
  1972. .sidebar-actions {
  1973. display: flex;
  1974. flex-direction: column;
  1975. gap: 12px;
  1976. padding-top: 16px;
  1977. border-top: 1px solid rgba(148, 163, 184, 0.2);
  1978. }
  1979. .new-research-btn {
  1980. display: flex;
  1981. align-items: center;
  1982. justify-content: center;
  1983. gap: 8px;
  1984. padding: 14px 20px;
  1985. background: linear-gradient(135deg, #3b82f6, #8b5cf6);
  1986. border: none;
  1987. border-radius: 12px;
  1988. color: white;
  1989. font-size: 15px;
  1990. font-weight: 600;
  1991. cursor: pointer;
  1992. transition: all 0.3s ease;
  1993. box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
  1994. }
  1995. .new-research-btn:hover {
  1996. transform: translateY(-2px);
  1997. box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
  1998. }
  1999. .new-research-btn:active {
  2000. transform: translateY(0);
  2001. }
  2002. /* 全屏状态下的结果面板 */
  2003. .layout-fullscreen .panel-result {
  2004. flex: 1;
  2005. height: 100vh;
  2006. border-radius: 0;
  2007. border: none;
  2008. overflow-y: auto;
  2009. max-width: none;
  2010. }
  2011. @media (max-width: 1024px) {
  2012. .sidebar {
  2013. width: 320px;
  2014. min-width: 320px;
  2015. }
  2016. }
  2017. @media (max-width: 768px) {
  2018. .layout-fullscreen {
  2019. flex-direction: column;
  2020. }
  2021. .sidebar {
  2022. width: 100%;
  2023. min-width: 100%;
  2024. height: auto;
  2025. max-height: 40vh;
  2026. }
  2027. .layout-fullscreen .panel-result {
  2028. height: 60vh;
  2029. }
  2030. }
  2031. </style>