pikamomo's picture
Initial deployment
a60c0af
<template>
<main class="app-shell" :class="{ expanded: isExpanded }">
<div class="aurora" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<!-- Initial state: centered input card -->
<div v-if="!isExpanded" class="layout layout-centered">
<section class="panel panel-form panel-centered">
<header class="panel-head">
<div class="logo">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
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"
/>
</svg>
</div>
<div>
<h1>Deep Research Assistant</h1>
<p>Combining multi-round intelligent search and summarization, presenting insights and citations in real-time.</p>
</div>
</header>
<form class="form" @submit.prevent="handleSubmit">
<label class="field">
<span>Research Topic</span>
<textarea
v-model="form.topic"
placeholder="e.g., Explore key breakthroughs in multimodal models in 2025"
rows="4"
required
></textarea>
</label>
<section class="options">
<label class="field option">
<span>Search Engine</span>
<select v-model="form.searchApi">
<option value="">Use backend default</option>
<option
v-for="option in searchOptions"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
</label>
</section>
<div class="form-actions">
<button class="submit" type="submit" :disabled="loading">
<span class="submit-label">
<svg
v-if="loading"
class="spinner"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" stroke-width="3" />
</svg>
{{ loading ? "Research in progress..." : "Start Research" }}
</span>
</button>
<button
v-if="loading"
type="button"
class="secondary-btn"
@click="cancelResearch"
>
Cancel Research
</button>
</div>
</form>
<p v-if="error" class="error-chip">
<svg viewBox="0 0 20 20" aria-hidden="true">
<path
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"
/>
</svg>
{{ error }}
</p>
<p v-else-if="loading" class="hint muted">
Collecting clues and evidence, real-time progress shown on the right.
</p>
</section>
</div>
<!-- Fullscreen state: left-right split layout -->
<div v-else class="layout layout-fullscreen">
<!-- Left side: Research info -->
<aside class="sidebar">
<div class="sidebar-header">
<button class="back-btn" @click="goBack" :disabled="loading">
<svg viewBox="0 0 24 24" width="20" height="20">
<path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<h2>🔍 Deep Research Assistant</h2>
</div>
<div class="research-info">
<div class="info-item">
<label>Research Topic</label>
<p class="topic-display">{{ form.topic }}</p>
</div>
<div class="info-item" v-if="form.searchApi">
<label>Search Engine</label>
<p>{{ form.searchApi }}</p>
</div>
<div class="info-item" v-if="totalTasks > 0">
<label>Research Progress</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></div>
</div>
<p class="progress-text">{{ completedTasks }} / {{ totalTasks }} tasks completed</p>
</div>
</div>
<div class="sidebar-actions">
<button class="new-research-btn" @click="startNewResearch">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
Start New Research
</button>
</div>
</aside>
<!-- Right side: Research results -->
<section
class="panel panel-result"
v-if="todoTasks.length || reportMarkdown || progressLogs.length"
>
<header class="status-bar">
<div class="status-main">
<div class="status-chip" :class="{ active: loading }">
<span class="dot"></span>
{{ loading ? "Research in progress" : "Research completed" }}
</div>
<span class="status-meta">
Task progress: {{ completedTasks }} / {{ totalTasks || todoTasks.length || 1 }}
· {{ progressLogs.length }} phase records
</span>
</div>
<div class="status-controls">
<button class="secondary-btn" @click="logsCollapsed = !logsCollapsed">
{{ logsCollapsed ? "Expand process" : "Collapse process" }}
</button>
</div>
</header>
<div class="timeline-wrapper" v-show="!logsCollapsed && progressLogs.length">
<transition-group name="timeline" tag="ul" class="timeline">
<li v-for="(log, index) in progressLogs" :key="`${log}-${index}`">
<span class="timeline-node"></span>
<p>{{ log }}</p>
</li>
</transition-group>
</div>
<div class="tasks-section" v-if="todoTasks.length">
<aside class="tasks-list">
<h3>Task List</h3>
<ul>
<li
v-for="task in todoTasks"
:key="task.id"
:class="['task-item', { active: task.id === activeTaskId, completed: task.status === 'completed' }]"
>
<button
type="button"
class="task-button"
@click="activeTaskId = task.id"
>
<span class="task-title">{{ task.title }}</span>
<span class="task-status" :class="task.status">
{{ formatTaskStatus(task.status) }}
</span>
</button>
<p class="task-intent">{{ task.intent }}</p>
</li>
</ul>
</aside>
<article class="task-detail" v-if="currentTask">
<header class="task-header">
<div>
<h3>{{ currentTaskTitle || "Current Task" }}</h3>
<p class="muted" v-if="currentTaskIntent">
{{ currentTaskIntent }}
</p>
</div>
<div class="task-chip-group">
<span class="task-label">Query: {{ currentTaskQuery || "" }}</span>
<span
v-if="currentTaskNoteId"
class="task-label note-chip"
:title="currentTaskNoteId"
>
Note: {{ currentTaskNoteId }}
</span>
<span
v-if="currentTaskNotePath"
class="task-label note-chip path-chip"
:title="currentTaskNotePath"
>
<span class="path-label">Path:</span>
<span class="path-text">{{ currentTaskNotePath }}</span>
<button
class="chip-action"
type="button"
@click="copyNotePath(currentTaskNotePath)"
>
Copy
</button>
</span>
</div>
</header>
<section v-if="currentTask && currentTask.notices.length" class="task-notices">
<h4>System Notices</h4>
<ul>
<li v-for="(notice, idx) in currentTask.notices" :key="`${notice}-${idx}`">
{{ notice }}
</li>
</ul>
</section>
<section
class="sources-block"
:class="{ 'block-highlight': sourcesHighlight }"
>
<h3>Latest Sources</h3>
<template v-if="currentTaskSources.length">
<ul class="sources-list">
<li
v-for="(item, index) in currentTaskSources"
:key="`${item.title}-${index}`"
class="source-item"
>
<a
class="source-link"
:href="item.url || '#'"
target="_blank"
rel="noopener noreferrer"
>
{{ item.title || item.url || `Source ${index + 1}` }}
</a>
<div v-if="item.snippet || item.raw" class="source-tooltip">
<p v-if="item.snippet">{{ item.snippet }}</p>
<p v-if="item.raw" class="muted-text">{{ item.raw }}</p>
</div>
</li>
</ul>
</template>
<p v-else class="muted">No sources available</p>
</section>
<section
class="summary-block"
:class="{ 'block-highlight': summaryHighlight }"
>
<h3>Task Summary</h3>
<pre class="block-pre">{{ currentTaskSummary || "No information available" }}</pre>
</section>
<section
class="tools-block"
:class="{ 'block-highlight': toolHighlight }"
v-if="currentTaskToolCalls.length"
>
<h3>Tool Call Records</h3>
<ul class="tool-list">
<li
v-for="entry in currentTaskToolCalls"
:key="`${entry.eventId}-${entry.timestamp}`"
class="tool-entry"
>
<div class="tool-entry-header">
<span class="tool-entry-title">
#{{ entry.eventId }} {{ entry.agent }}{{ entry.tool }}
</span>
<span
v-if="entry.noteId"
class="tool-entry-note"
>
Note: {{ entry.noteId }}
</span>
</div>
<p v-if="entry.notePath" class="tool-entry-path">
Note path:
<button
class="link-btn"
type="button"
@click="copyNotePath(entry.notePath)"
>
Copy
</button>
<span class="path-text">{{ entry.notePath }}</span>
</p>
<p class="tool-subtitle">Parameters</p>
<pre class="tool-pre">{{ formatToolParameters(entry.parameters) }}</pre>
<template v-if="entry.result">
<p class="tool-subtitle">Execution Result</p>
<pre class="tool-pre">{{ formatToolResult(entry.result) }}</pre>
</template>
</li>
</ul>
</section>
</article>
<article class="task-detail" v-else>
<p class="muted">Waiting for task planning or execution results.</p>
</article>
</div>
<div
v-if="reportMarkdown"
class="report-block"
:class="{ 'block-highlight': reportHighlight }"
>
<h3>Final Report</h3>
<pre class="block-pre">{{ reportMarkdown }}</pre>
</div>
</section>
</div>
</main>
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, reactive, ref } from "vue";
import {
runResearchStream,
type ResearchStreamEvent
} from "./services/api";
interface SourceItem {
title: string;
url: string;
snippet: string;
raw: string;
}
interface ToolCallLog {
eventId: number;
agent: string;
tool: string;
parameters: Record<string, unknown>;
result: string;
noteId: string | null;
notePath: string | null;
timestamp: number;
}
interface TodoTaskView {
id: number;
title: string;
intent: string;
query: string;
status: string;
summary: string;
sourcesSummary: string;
sourceItems: SourceItem[];
notices: string[];
noteId: string | null;
notePath: string | null;
toolCalls: ToolCallLog[];
}
const form = reactive({
topic: "",
searchApi: ""
});
const loading = ref(false);
const error = ref("");
const progressLogs = ref<string[]>([]);
const logsCollapsed = ref(false);
const isExpanded = ref(false);
const todoTasks = ref<TodoTaskView[]>([]);
const activeTaskId = ref<number | null>(null);
const reportMarkdown = ref("");
const summaryHighlight = ref(false);
const sourcesHighlight = ref(false);
const reportHighlight = ref(false);
const toolHighlight = ref(false);
let currentController: AbortController | null = null;
const searchOptions = [
"advanced",
"duckduckgo",
"tavily",
"perplexity",
"searxng"
];
const TASK_STATUS_LABEL: Record<string, string> = {
pending: "Pending",
in_progress: "In Progress",
completed: "Completed",
skipped: "Skipped"
};
function formatTaskStatus(status: string): string {
return TASK_STATUS_LABEL[status] ?? status;
}
const totalTasks = computed(() => todoTasks.value.length);
const completedTasks = computed(() =>
todoTasks.value.filter((task) => task.status === "completed").length
);
const currentTask = computed(() => {
if (activeTaskId.value !== null) {
return todoTasks.value.find((task) => task.id === activeTaskId.value) ?? null;
}
return todoTasks.value[0] ?? null;
});
const currentTaskSources = computed(() => currentTask.value?.sourceItems ?? []);
const currentTaskSummary = computed(() => currentTask.value?.summary ?? "");
const currentTaskTitle = computed(() => currentTask.value?.title ?? "");
const currentTaskIntent = computed(() => currentTask.value?.intent ?? "");
const currentTaskQuery = computed(() => currentTask.value?.query ?? "");
const currentTaskNoteId = computed(() => currentTask.value?.noteId ?? "");
const currentTaskNotePath = computed(() => currentTask.value?.notePath ?? "");
const currentTaskToolCalls = computed(
() => currentTask.value?.toolCalls ?? []
);
const pulse = (flag: typeof summaryHighlight) => {
flag.value = false;
requestAnimationFrame(() => {
flag.value = true;
window.setTimeout(() => {
flag.value = false;
}, 1200);
});
};
function parseSources(raw: string): SourceItem[] {
if (!raw) {
return [];
}
const items: SourceItem[] = [];
const lines = raw.split("\n");
let current: SourceItem | null = null;
const truncate = (value: string, max = 360) => {
const trimmed = value.trim();
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
};
const flush = () => {
if (!current) {
return;
}
const normalized: SourceItem = {
title: current.title?.trim() || "",
url: current.url?.trim() || "",
snippet: current.snippet ? truncate(current.snippet) : "",
raw: current.raw ? truncate(current.raw, 420) : ""
};
if (
normalized.title ||
normalized.url ||
normalized.snippet ||
normalized.raw
) {
if (!normalized.title && normalized.url) {
normalized.title = normalized.url;
}
items.push(normalized);
}
current = null;
};
const ensureCurrent = () => {
if (!current) {
current = { title: "", url: "", snippet: "", raw: "" };
}
};
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
if (/^\*/.test(trimmed) && trimmed.includes(" : ")) {
flush();
const withoutBullet = trimmed.replace(/^\*\s*/, "");
const [titlePart, urlPart] = withoutBullet.split(" : ");
current = {
title: titlePart?.trim() || "",
url: urlPart?.trim() || "",
snippet: "",
raw: ""
};
continue;
}
if (/^(Source|Info Source)\s*:/.test(trimmed)) {
flush();
const [, titlePart = ""] = trimmed.split(/:\s*(.+)/);
current = {
title: titlePart.trim(),
url: "",
snippet: "",
raw: ""
};
continue;
}
if (/^URL\s*:/.test(trimmed)) {
ensureCurrent();
const [, urlPart = ""] = trimmed.split(/:\s*(.+)/);
current!.url = urlPart.trim();
continue;
}
if (
/^(Most relevant content from source|Info Content)\s*:/.test(trimmed)
) {
ensureCurrent();
const [, contentPart = ""] = trimmed.split(/:\s*(.+)/);
current!.snippet = contentPart.trim();
continue;
}
if (
/^(Full source content limited to|Info content limited to)\s*:/.test(trimmed)
) {
ensureCurrent();
const [, rawPart = ""] = trimmed.split(/:\s*(.+)/);
current!.raw = rawPart.trim();
continue;
}
if (/^https?:\/\//.test(trimmed)) {
ensureCurrent();
if (!current!.url) {
current!.url = trimmed;
continue;
}
}
ensureCurrent();
current!.raw = current!.raw ? `${current!.raw}\n${trimmed}` : trimmed;
}
flush();
return items;
}
function extractOptionalString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function ensureRecord(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return {};
}
function applyNoteMetadata(
task: TodoTaskView,
payload: Record<string, unknown>
): void {
const noteId = extractOptionalString(payload.note_id);
if (noteId) {
task.noteId = noteId;
}
const notePath = extractOptionalString(payload.note_path);
if (notePath) {
task.notePath = notePath;
}
}
function formatToolParameters(parameters: Record<string, unknown>): string {
try {
return JSON.stringify(parameters, null, 2);
} catch (error) {
console.warn("Unable to format tool parameters", error, parameters);
return Object.entries(parameters)
.map(([key, value]) => `${key}: ${String(value)}`)
.join("\n");
}
}
function formatToolResult(result: string): string {
const trimmed = result.trim();
const limit = 900;
if (trimmed.length > limit) {
return `${trimmed.slice(0, limit)}…`;
}
return trimmed;
}
async function copyNotePath(path: string | null | undefined) {
if (!path) {
return;
}
try {
await navigator.clipboard.writeText(path);
progressLogs.value.push(`Note path copied: ${path}`);
} catch (error) {
console.warn("Unable to copy to clipboard directly", error);
window.prompt("Copy the following note path", path);
progressLogs.value.push("Please copy the note path manually");
}
}
function resetWorkflowState() {
todoTasks.value = [];
activeTaskId.value = null;
reportMarkdown.value = "";
progressLogs.value = [];
summaryHighlight.value = false;
sourcesHighlight.value = false;
reportHighlight.value = false;
toolHighlight.value = false;
logsCollapsed.value = false;
}
function findTask(taskId: unknown): TodoTaskView | undefined {
const numeric =
typeof taskId === "number"
? taskId
: typeof taskId === "string"
? Number(taskId)
: NaN;
if (Number.isNaN(numeric)) {
return undefined;
}
return todoTasks.value.find((task) => task.id === numeric);
}
function upsertTaskMetadata(task: TodoTaskView, payload: Record<string, unknown>) {
if (typeof payload.title === "string" && payload.title.trim()) {
task.title = payload.title.trim();
}
if (typeof payload.intent === "string" && payload.intent.trim()) {
task.intent = payload.intent.trim();
}
if (typeof payload.query === "string" && payload.query.trim()) {
task.query = payload.query.trim();
}
}
const handleSubmit = async () => {
if (!form.topic.trim()) {
error.value = "Please enter a research topic";
return;
}
if (currentController) {
currentController.abort();
currentController = null;
}
loading.value = true;
error.value = "";
isExpanded.value = true;
resetWorkflowState();
const controller = new AbortController();
currentController = controller;
const payload = {
topic: form.topic.trim(),
search_api: form.searchApi || undefined
};
try {
await runResearchStream(
payload,
(event: ResearchStreamEvent) => {
if (event.type === "status") {
const message =
typeof event.message === "string" && event.message.trim()
? event.message
: "Workflow status update";
progressLogs.value.push(message);
const payload = event as Record<string, unknown>;
const task = findTask(payload.task_id);
if (task && message) {
task.notices.push(message);
applyNoteMetadata(task, payload);
}
return;
}
if (event.type === "todo_list") {
const tasks = Array.isArray(event.tasks)
? (event.tasks as Record<string, unknown>[])
: [];
todoTasks.value = tasks.map((item, index) => {
const rawId =
typeof item.id === "number"
? item.id
: typeof item.id === "string"
? Number(item.id)
: index + 1;
const id = Number.isFinite(rawId) ? Number(rawId) : index + 1;
const noteId =
typeof item.note_id === "string" && item.note_id.trim()
? item.note_id.trim()
: null;
const notePath =
typeof item.note_path === "string" && item.note_path.trim()
? item.note_path.trim()
: null;
return {
id,
title:
typeof item.title === "string" && item.title.trim()
? item.title.trim()
: `Task ${id}`,
intent:
typeof item.intent === "string" && item.intent.trim()
? item.intent.trim()
: "Explore key information related to the topic",
query:
typeof item.query === "string" && item.query.trim()
? item.query.trim()
: form.topic.trim(),
status:
typeof item.status === "string" && item.status.trim()
? item.status.trim()
: "pending",
summary: "",
sourcesSummary: "",
sourceItems: [],
notices: [],
noteId,
notePath,
toolCalls: []
} as TodoTaskView;
});
if (todoTasks.value.length) {
activeTaskId.value = todoTasks.value[0].id;
progressLogs.value.push("Task list generated");
} else {
progressLogs.value.push("No task list generated, continuing with default task");
}
return;
}
if (event.type === "task_status") {
const payload = event as Record<string, unknown>;
const task = findTask(event.task_id);
if (!task) {
return;
}
upsertTaskMetadata(task, payload);
applyNoteMetadata(task, payload);
const status =
typeof event.status === "string" && event.status.trim()
? event.status.trim()
: task.status;
task.status = status;
if (status === "in_progress") {
task.summary = "";
task.sourcesSummary = "";
task.sourceItems = [];
task.notices = [];
activeTaskId.value = task.id;
progressLogs.value.push(`Starting task: ${task.title}`);
} else if (status === "completed") {
if (typeof event.summary === "string" && event.summary.trim()) {
task.summary = event.summary.trim();
}
if (
typeof event.sources_summary === "string" &&
event.sources_summary.trim()
) {
task.sourcesSummary = event.sources_summary.trim();
task.sourceItems = parseSources(task.sourcesSummary);
}
progressLogs.value.push(`Completed task: ${task.title}`);
if (activeTaskId.value === task.id) {
pulse(summaryHighlight);
pulse(sourcesHighlight);
}
} else if (status === "skipped") {
progressLogs.value.push(`Task skipped: ${task.title}`);
}
return;
}
if (event.type === "sources") {
const payload = event as Record<string, unknown>;
const task = findTask(event.task_id);
if (!task) {
return;
}
const textCandidates = [
payload.latest_sources,
payload.sources_summary,
payload.raw_context
];
const latestText = textCandidates
.map((value) => (typeof value === "string" ? value.trim() : ""))
.find((value) => value);
if (latestText) {
task.sourcesSummary = latestText;
task.sourceItems = parseSources(latestText);
if (activeTaskId.value === task.id) {
pulse(sourcesHighlight);
}
progressLogs.value.push(`Updated task sources: ${task.title}`);
}
if (typeof payload.backend === "string") {
progressLogs.value.push(
`Current search backend: ${payload.backend}`
);
}
applyNoteMetadata(task, payload);
return;
}
if (event.type === "task_summary_chunk") {
const payload = event as Record<string, unknown>;
const task = findTask(event.task_id);
if (!task) {
return;
}
const chunk =
typeof event.content === "string" ? event.content : "";
task.summary += chunk;
applyNoteMetadata(task, payload);
if (activeTaskId.value === task.id) {
pulse(summaryHighlight);
}
return;
}
if (event.type === "tool_call") {
const payload = event as Record<string, unknown>;
const eventId =
typeof payload.event_id === "number"
? payload.event_id
: Date.now();
const agent =
typeof payload.agent === "string" && payload.agent.trim()
? payload.agent.trim()
: "Agent";
const tool =
typeof payload.tool === "string" && payload.tool.trim()
? payload.tool.trim()
: "tool";
const parameters = ensureRecord(payload.parameters);
const result =
typeof payload.result === "string" ? payload.result : "";
const noteId = extractOptionalString(payload.note_id);
const notePath = extractOptionalString(payload.note_path);
const task = findTask(payload.task_id);
if (task) {
task.toolCalls.push({
eventId,
agent,
tool,
parameters,
result,
noteId,
notePath,
timestamp: Date.now()
});
if (noteId) {
task.noteId = noteId;
}
if (notePath) {
task.notePath = notePath;
}
const logSummary = noteId
? `${agent} called ${tool} (Task ${task.id}, Note ${noteId})`
: `${agent} called ${tool} (Task ${task.id})`;
progressLogs.value.push(logSummary);
if (activeTaskId.value === task.id) {
pulse(toolHighlight);
}
} else {
progressLogs.value.push(`${agent} called ${tool}`);
}
return;
}
if (event.type === "final_report") {
const report =
typeof event.report === "string" && event.report.trim()
? event.report.trim()
: "";
reportMarkdown.value = report || "Report generation failed, no valid content obtained";
pulse(reportHighlight);
progressLogs.value.push("Final report generated");
return;
}
if (event.type === "error") {
const detail =
typeof event.detail === "string" && event.detail.trim()
? event.detail
: "An error occurred during research";
error.value = detail;
progressLogs.value.push("Research failed, workflow stopped");
}
},
{ signal: controller.signal }
);
if (!reportMarkdown.value) {
reportMarkdown.value = "No report generated";
}
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
progressLogs.value.push("Current research task cancelled");
} else {
error.value = err instanceof Error ? err.message : "Request failed";
}
} finally {
loading.value = false;
if (currentController === controller) {
currentController = null;
}
}
};
const cancelResearch = () => {
if (!loading.value || !currentController) {
return;
}
progressLogs.value.push("Attempting to cancel current research task...");
currentController.abort();
};
const goBack = () => {
if (loading.value) {
return; // Cannot go back while research is in progress
}
isExpanded.value = false;
};
const startNewResearch = () => {
if (loading.value) {
cancelResearch();
}
resetWorkflowState();
isExpanded.value = false;
form.topic = "";
form.searchApi = "";
};
onBeforeUnmount(() => {
if (currentController) {
currentController.abort();
currentController = null;
}
});
</script>
<style scoped>
.app-shell {
position: relative;
min-height: 100vh;
padding: 72px 24px;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
color: #1f2937;
overflow: hidden;
box-sizing: border-box;
transition: padding 0.4s ease;
}
.app-shell.expanded {
padding: 0;
align-items: stretch;
}
.aurora {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.55;
}
.aurora span {
position: absolute;
width: 45vw;
height: 45vw;
max-width: 520px;
max-height: 520px;
background: radial-gradient(circle, rgba(148, 197, 255, 0.35), transparent 60%);
filter: blur(90px);
animation: float 26s infinite linear;
}
.aurora span:nth-child(1) {
top: -20%;
left: -18%;
animation-delay: 0s;
}
.aurora span:nth-child(2) {
bottom: -25%;
right: -20%;
background: radial-gradient(circle, rgba(166, 139, 255, 0.28), transparent 60%);
animation-delay: -9s;
}
.aurora span:nth-child(3) {
top: 35%;
left: 45%;
background: radial-gradient(circle, rgba(164, 219, 216, 0.26), transparent 60%);
animation-delay: -16s;
}
.layout {
position: relative;
width: 100%;
display: flex;
gap: 24px;
z-index: 1;
transition: all 0.4s ease;
}
.layout-centered {
max-width: 600px;
justify-content: center;
align-items: center;
}
.layout-fullscreen {
height: 100vh;
max-width: 100%;
gap: 0;
align-items: stretch;
}
.panel {
position: relative;
flex: 1 1 360px;
padding: 24px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.12);
backdrop-filter: blur(8px);
overflow: hidden;
}
.panel-form {
max-width: 420px;
}
.panel-centered {
width: 100%;
max-width: 600px;
padding: 40px;
box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
transform: scale(1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.panel-centered:hover {
transform: scale(1.02);
box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
}
.panel-result {
min-width: 360px;
flex: 2 1 420px;
}
.panel::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(125, 86, 255, 0.1));
opacity: 0;
transition: opacity 0.35s ease;
z-index: 0;
}
.panel:hover::before {
opacity: 1;
}
.panel > * {
position: relative;
z-index: 1;
}
.panel-form h1 {
margin: 0;
font-size: 26px;
letter-spacing: 0.01em;
}
.panel-form p {
margin: 4px 0 0;
color: #64748b;
font-size: 13px;
}
.panel-head {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.logo {
width: 52px;
height: 52px;
display: grid;
place-items: center;
border-radius: 16px;
background: linear-gradient(135deg, #2563eb, #7c3aed);
box-shadow: 0 12px 28px rgba(59, 130, 246, 0.4);
}
.logo svg {
width: 28px;
height: 28px;
fill: #f8fafc;
}
.form {
display: flex;
flex-direction: column;
gap: 18px;
}
.field {
display: flex;
flex-direction: column;
gap: 10px;
}
.field span {
font-weight: 600;
color: #475569;
}
textarea,
input,
select {
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(255, 255, 255, 0.92);
color: #1f2937;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
textarea:focus,
input:focus,
select:focus {
outline: none;
border-color: rgba(37, 99, 235, 0.65);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background: #ffffff;
}
.options {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.option {
flex: 1;
min-width: 140px;
}
.form-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.submit {
align-self: flex-start;
padding: 12px 24px;
border-radius: 16px;
border: none;
background: linear-gradient(135deg, #2563eb, #7c3aed);
color: #ffffff;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
display: inline-flex;
align-items: center;
gap: 10px;
position: relative;
}
.submit-label {
display: inline-flex;
align-items: center;
gap: 10px;
}
.submit .spinner {
width: 18px;
height: 18px;
fill: none;
stroke: rgba(255, 255, 255, 0.85);
stroke-linecap: round;
animation: spin 1s linear infinite;
}
.submit:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.submit:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
}
.secondary-btn {
padding: 10px 18px;
border-radius: 14px;
background: rgba(148, 163, 184, 0.12);
border: 1px solid rgba(148, 163, 184, 0.28);
color: #1f2937;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.secondary-btn:hover {
background: rgba(148, 163, 184, 0.2);
border-color: rgba(148, 163, 184, 0.35);
color: #0f172a;
}
.error-chip {
margin-top: 16px;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: rgba(248, 113, 113, 0.12);
border: 1px solid rgba(248, 113, 113, 0.35);
border-radius: 14px;
color: #b91c1c;
font-size: 14px;
}
.error-chip svg {
width: 18px;
height: 18px;
fill: currentColor;
}
.panel-result {
display: flex;
flex-direction: column;
gap: 18px;
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.status-main {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.status-controls {
display: flex;
gap: 8px;
}
.status-chip {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(191, 219, 254, 0.28);
padding: 8px 14px;
border-radius: 999px;
font-size: 13px;
color: #1f2937;
border: 1px solid rgba(59, 130, 246, 0.35);
transition: background 0.3s ease, color 0.3s ease;
}
.status-chip.active {
background: rgba(129, 140, 248, 0.2);
border-color: rgba(129, 140, 248, 0.4);
color: #1e293b;
}
.status-chip .dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #2563eb;
box-shadow: 0 0 12px rgba(37, 99, 235, 0.45);
animation: pulse 1.8s ease-in-out infinite;
}
.status-meta {
color: #64748b;
font-size: 13px;
}
.timeline-wrapper {
margin-top: 12px;
max-height: 220px;
overflow-y: auto;
padding-right: 8px;
scrollbar-width: thin;
scrollbar-color: rgba(129, 140, 248, 0.45) rgba(226, 232, 240, 0.6);
}
.timeline-wrapper::-webkit-scrollbar {
width: 6px;
}
.timeline-wrapper::-webkit-scrollbar-track {
background: rgba(226, 232, 240, 0.6);
border-radius: 999px;
}
.timeline-wrapper::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(129, 140, 248, 0.8), rgba(59, 130, 246, 0.7));
border-radius: 999px;
}
.timeline-wrapper::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(99, 102, 241, 0.9), rgba(37, 99, 235, 0.8));
}
.timeline {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 14px;
position: relative;
padding-left: 12px;
}
.timeline::before {
content: "";
position: absolute;
top: 8px;
bottom: 8px;
left: 0;
width: 2px;
background: linear-gradient(180deg, rgba(59, 130, 246, 0.35), rgba(129, 140, 248, 0.15));
}
.timeline li {
position: relative;
padding-left: 24px;
color: #1e293b;
font-size: 14px;
line-height: 1.5;
}
.timeline-node {
position: absolute;
left: -12px;
top: 6px;
width: 10px;
height: 10px;
border-radius: 999px;
background: linear-gradient(135deg, #38bdf8, #7c3aed);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
}
.timeline-enter-active,
.timeline-leave-active {
transition: all 0.35s ease, opacity 0.35s ease;
}
.timeline-enter-from,
.timeline-leave-to {
opacity: 0;
transform: translateY(-6px);
}
.tasks-section {
display: grid;
grid-template-columns: 280px 1fr;
gap: 20px;
align-items: start;
}
@media (max-width: 960px) {
.tasks-section {
grid-template-columns: 1fr;
}
}
.tasks-list {
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(148, 163, 184, 0.26);
border-radius: 18px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
}
.tasks-list h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.tasks-list ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.task-item {
border-radius: 14px;
border: 1px solid transparent;
transition: border-color 0.2s ease, background 0.2s ease;
}
.task-item.completed {
border-color: rgba(56, 189, 248, 0.35);
background: rgba(191, 219, 254, 0.28);
}
.task-item.active {
border-color: rgba(129, 140, 248, 0.5);
background: rgba(224, 231, 255, 0.5);
}
.task-button {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px 6px;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
text-align: left;
}
.task-title {
font-weight: 600;
font-size: 14px;
color: #1e293b;
}
.task-status {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
color: #1f2937;
background: rgba(148, 163, 184, 0.2);
}
.task-status.pending {
background: rgba(148, 163, 184, 0.18);
color: #475569;
}
.task-status.in_progress {
background: rgba(129, 140, 248, 0.24);
color: #312e81;
}
.task-status.completed {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
.task-status.skipped {
background: rgba(248, 113, 113, 0.18);
color: #b91c1c;
}
.task-intent {
margin: 0;
padding: 0 14px 12px 14px;
font-size: 13px;
color: #64748b;
}
.task-detail {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(148, 163, 184, 0.26);
border-radius: 18px;
padding: 22px;
display: flex;
flex-direction: column;
gap: 18px;
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.5);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 12px;
}
.task-chip-group {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.task-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
.task-header .muted {
margin: 6px 0 0;
}
.task-label {
padding: 6px 12px;
border-radius: 999px;
background: rgba(191, 219, 254, 0.32);
border: 1px solid rgba(59, 130, 246, 0.35);
font-size: 12px;
color: #1e3a8a;
}
.task-label.note-chip {
background: rgba(34, 197, 94, 0.2);
border-color: rgba(34, 197, 94, 0.35);
color: #15803d;
}
.task-label.path-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 360px;
background: rgba(56, 189, 248, 0.2);
border-color: rgba(56, 189, 248, 0.35);
color: #0369a1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.path-label {
font-weight: 500;
}
.path-text {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip-action {
border: none;
background: rgba(56, 189, 248, 0.2);
color: #0369a1;
padding: 3px 8px;
border-radius: 10px;
font-size: 11px;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.chip-action:hover {
background: rgba(14, 165, 233, 0.28);
color: #0f172a;
}
.task-notices {
background: rgba(191, 219, 254, 0.28);
border: 1px solid rgba(96, 165, 250, 0.35);
border-radius: 16px;
padding: 14px 18px;
color: #1f2937;
}
.task-notices h4 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
}
.task-notices ul {
list-style: disc;
margin: 0 0 0 18px;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.task-notices li {
font-size: 13px;
}
.report-block {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(148, 163, 184, 0.26);
border-radius: 18px;
padding: 22px;
display: flex;
flex-direction: column;
gap: 12px;
}
.report-block h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
.block-pre {
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 13px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
color: #1f2937;
background: rgba(248, 250, 252, 0.9);
padding: 16px;
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.35);
overflow: auto;
max-height: 420px;
scrollbar-width: thin;
scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
}
.block-pre::-webkit-scrollbar {
width: 6px;
}
.block-pre::-webkit-scrollbar-track {
background: rgba(226, 232, 240, 0.7);
border-radius: 999px;
}
.block-pre::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(99, 102, 241, 0.75), rgba(59, 130, 246, 0.65));
border-radius: 999px;
}
.block-pre::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(79, 70, 229, 0.8), rgba(37, 99, 235, 0.75));
}
.summary-block .block-pre,
.sources-block .block-pre {
max-height: 360px;
}
.tools-block {
position: relative;
margin-top: 16px;
padding: 20px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
display: flex;
flex-direction: column;
gap: 12px;
}
.tools-block h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
letter-spacing: 0.02em;
}
.tool-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.tool-entry {
background: rgba(248, 250, 252, 0.95);
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 14px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.tool-entry-header {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.tool-entry-title {
font-weight: 600;
color: #1f2937;
}
.tool-entry-note {
font-size: 12px;
color: #0f766e;
}
.tool-entry-path {
margin: 0;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
color: #2563eb;
}
.tool-subtitle {
margin: 0;
font-size: 13px;
color: #475569;
font-weight: 500;
}
.tool-pre {
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: #1f2937;
background: rgba(248, 250, 252, 0.9);
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.28);
overflow: auto;
max-height: 260px;
scrollbar-width: thin;
scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
}
.tool-pre::-webkit-scrollbar {
width: 6px;
}
.tool-pre::-webkit-scrollbar-track {
background: rgba(226, 232, 240, 0.7);
}
.tool-pre::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.7);
border-radius: 10px;
}
.link-btn {
background: none;
border: none;
color: #0369a1;
cursor: pointer;
padding: 0 4px;
font-size: 12px;
border-radius: 8px;
transition: color 0.2s ease, background 0.2s ease;
}
.link-btn:hover {
color: #0ea5e9;
background: rgba(14, 165, 233, 0.16);
}
.sources-block,
.summary-block {
position: relative;
margin-top: 16px;
padding: 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
}
.sources-history {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.sources-history h4 {
margin: 0;
color: #1f2937;
font-size: 14px;
letter-spacing: 0.01em;
}
.history-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.history-list details {
background: rgba(248, 250, 252, 0.95);
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 14px;
padding: 12px 16px;
color: #1f2937;
transition: border-color 0.2s ease, background 0.2s ease;
}
.history-list details[open] {
background: rgba(224, 231, 255, 0.55);
border-color: rgba(129, 140, 248, 0.4);
}
.history-list summary {
cursor: pointer;
font-weight: 600;
outline: none;
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
}
.history-list summary::-webkit-details-marker {
display: none;
}
.history-list summary::after {
content: "▾";
margin-left: 6px;
font-size: 12px;
opacity: 0.7;
transition: transform 0.2s ease;
}
.history-list details[open] summary::after {
transform: rotate(180deg);
}
.block-highlight {
animation: glow 1.2s ease;
}
.sources-block h3,
.summary-block h3 {
margin: 0 0 14px;
color: #1f2937;
letter-spacing: 0.02em;
}
.sources-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.source-item {
position: relative;
display: inline-flex;
flex-direction: column;
gap: 6px;
}
.source-link {
color: #2563eb;
text-decoration: none;
font-weight: 600;
letter-spacing: 0.01em;
transition: color 0.2s ease;
}
.source-link::after {
content: " ↗";
font-size: 12px;
opacity: 0.6;
}
.source-link:hover {
color: #0f172a;
}
.source-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 12px);
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.98);
color: #1f2937;
padding: 14px 16px;
border-radius: 16px;
box-shadow: 0 18px 32px rgba(15, 23, 42, 0.18);
width: min(420px, 90vw);
z-index: 20;
border: 1px solid rgba(148, 163, 184, 0.24);
}
.source-tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 10px;
border-style: solid;
border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
}
.source-tooltip::before {
content: "";
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border-width: 12px 10px 0 10px;
border-style: solid;
border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
filter: drop-shadow(0 -2px 4px rgba(15, 23, 42, 0.12));
}
.source-tooltip p {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.6;
}
.source-tooltip p:last-child {
margin-bottom: 0;
}
.muted-text {
color: #64748b;
}
.source-item:hover .source-tooltip,
.source-item:focus-within .source-tooltip {
display: block;
}
.hint.muted {
color: #64748b;
}
@keyframes float {
0% {
transform: translate3d(0, 0, 0) rotate(0deg);
}
50% {
transform: translate3d(10%, 6%, 0) rotate(3deg);
}
100% {
transform: translate3d(0, 0, 0) rotate(0deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.5;
}
}
@keyframes glow {
0% {
box-shadow: 0 0 0 rgba(59, 130, 246, 0.3);
border-color: rgba(59, 130, 246, 0.5);
}
100% {
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.12);
border-color: rgba(148, 163, 184, 0.2);
}
}
@media (max-width: 960px) {
.app-shell {
padding: 56px 16px;
}
.layout {
flex-direction: column;
align-items: stretch;
}
.panel {
padding: 22px;
}
.panel-form,
.panel-result {
max-width: none;
}
.status-bar {
flex-direction: column;
align-items: flex-start;
}
.status-main,
.status-controls {
width: 100%;
}
.status-controls {
justify-content: flex-start;
}
}
@media (max-width: 600px) {
.options {
flex-direction: column;
}
.status-meta {
font-size: 12px;
}
.panel-head {
flex-direction: column;
align-items: flex-start;
}
.panel-form h1 {
font-size: 24px;
}
}
/* Sidebar styles */
.sidebar {
width: 400px;
min-width: 400px;
height: 100vh;
background: rgba(255, 255, 255, 0.98);
border-right: 1px solid rgba(148, 163, 184, 0.2);
padding: 32px 24px;
display: flex;
flex-direction: column;
gap: 24px;
overflow-y: auto;
box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
}
.sidebar-header {
display: flex;
flex-direction: column;
gap: 16px;
}
.sidebar-header h2 {
font-size: 24px;
font-weight: 700;
margin: 0;
color: #1f2937;
}
.back-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: transparent;
border: 1px solid rgba(148, 163, 184, 0.3);
border-radius: 12px;
color: #64748b;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: fit-content;
}
.back-btn:hover:not(:disabled) {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
.back-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.research-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-item label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #64748b;
}
.info-item p {
margin: 0;
font-size: 14px;
color: #1f2937;
line-height: 1.6;
}
.topic-display {
font-size: 16px !important;
font-weight: 600;
color: #0f172a !important;
padding: 12px;
background: rgba(59, 130, 246, 0.05);
border-radius: 8px;
border-left: 3px solid #3b82f6;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(148, 163, 184, 0.2);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 4px;
transition: width 0.5s ease;
}
.progress-text {
font-size: 13px !important;
color: #64748b !important;
font-weight: 500;
}
.sidebar-actions {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 16px;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.new-research-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 20px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border: none;
border-radius: 12px;
color: white;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.new-research-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
}
.new-research-btn:active {
transform: translateY(0);
}
/* Fullscreen result panel styles */
.layout-fullscreen .panel-result {
flex: 1;
height: 100vh;
border-radius: 0;
border: none;
overflow-y: auto;
max-width: none;
}
@media (max-width: 1024px) {
.sidebar {
width: 320px;
min-width: 320px;
}
}
@media (max-width: 768px) {
.layout-fullscreen {
flex-direction: column;
}
.sidebar {
width: 100%;
min-width: 100%;
height: auto;
max-height: 40vh;
}
.layout-fullscreen .panel-result {
height: 60vh;
}
}
</style>