Commit 0a63c6b5 authored by edy's avatar edy

feat(ui): add task panel view

parent 613f5921
Pipeline #18458 failed
......@@ -2544,6 +2544,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
ipcMain.handle(IPC_CHANNELS.expertsList, async () => expertCatalogService.list());
ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary());
ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary);
ipcMain.handle(IPC_CHANNELS.tasksListByDate, async () => []);
ipcMain.handle(IPC_CHANNELS.skillCatalogList, async () => skillCatalogService.listForActiveProject());
ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects());
......@@ -2673,6 +2674,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
system: {
getSummary: () => Promise.resolve(systemSummary)
},
tasks: {
listByDate: async () => []
},
chat: {
listSessions: async () => {
const sessions = await listSessionsForActiveProject(projectStore);
......
......@@ -78,6 +78,9 @@ const desktopApi: DesktopApi = {
system: {
getSummary: () => ipcRenderer.invoke(IPC_CHANNELS.systemGetSummary)
},
tasks: {
listByDate: (date: string) => ipcRenderer.invoke(IPC_CHANNELS.tasksListByDate, date)
},
chat: {
listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.chatListSessions),
listSessionsByProject: (projectId: string) => ipcRenderer.invoke(IPC_CHANNELS.chatListSessionsByProject, projectId),
......
......@@ -46,6 +46,7 @@ import { useMessageTraces } from "./features/chat/useMessageTraces";
import { usePromptSubmission } from "./features/chat/usePromptSubmission";
import { useSessionMessageStore } from "./features/chat/useSessionMessageStore";
import { KnowledgeView } from "./features/knowledge/KnowledgeView";
import { TaskPanelView } from "./features/tasks/TaskPanelView";
import { getPluginCopy, getPluginStatusLabel, getPluginTone, groupPluginsByStatus } from "./features/plugins/pluginDisplay";
import { PluginsView } from "./features/plugins/PluginsView";
import { AppSidebar } from "./features/shell/AppSidebar";
......@@ -97,7 +98,7 @@ import {
} from "./lib/constants";
import { desktopApi, isMockDesktopApi, smokeEnabled } from "./lib/desktop-api";
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge";
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge";
type SendPhase = "idle" | "preparing" | "streaming" | "finalizing";
type MessageReaction = "up" | "down";
......@@ -1380,6 +1381,9 @@ export default function App() {
{viewMode === "knowledge" ? (
<KnowledgeView />
) : null}
{viewMode === "tasks" ? (
<TaskPanelView />
) : null}
{viewMode === "settings" ? (
<SettingsView statusHint={settingsStatusHint}>
<SettingsPanels {...settingsPanelsProps} />
......
......@@ -234,7 +234,7 @@ export function getIntentSuggestionIcon(platform?: string): ReactNode {
return <BrowserExpertIcon />;
}
export function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "settings" | "knowledge" }) {
export function NavIcon({ kind }: { kind: "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge" }) {
switch (kind) {
case "chat":
return (
......@@ -250,6 +250,14 @@ export function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "sett
<path d="m12 7.1.78 1.58 1.75.25-1.27 1.24.3 1.74L12 11.09l-1.56.82.3-1.74-1.27-1.24 1.75-.25L12 7.1Z" fill="#F59E0B" />
</svg>
);
case "tasks":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M6.2 4.25h11.6A2.45 2.45 0 0 1 20.25 6.7v10.6a2.45 2.45 0 0 1-2.45 2.45H6.2a2.45 2.45 0 0 1-2.45-2.45V6.7A2.45 2.45 0 0 1 6.2 4.25Z" fill="#ECFDF5" stroke="#059669" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.45" />
<path d="M7.4 8.05h1.55l.72 1.12 1.6-2.18M13.05 8.25h3.8M7.4 13h1.55l.72 1.12 1.6-2.18M13.05 13.2h3.8" fill="none" stroke="#047857" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M13.05 16.95h2.8" fill="none" stroke="#2563EB" strokeLinecap="round" strokeWidth="1.35" />
</svg>
);
case "plugins":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
......
......@@ -13,7 +13,7 @@ import type {
WorkspaceSummary
} from "@qjclaw/shared-types"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
interface BootstrapSkill {
id: string
......
......@@ -22,7 +22,7 @@ interface ChatComposerProps {
canSend: boolean
isDragOver: boolean
isResizeActive: boolean
viewMode: "chat" | "experts" | "plugins" | "settings" | "knowledge"
viewMode: "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
shellStyle: CSSProperties
attachmentInputRef: RefObject<HTMLInputElement | null>
skillMenuRef: RefObject<HTMLDivElement | null>
......
......@@ -19,7 +19,7 @@ import { ConversationStatusNotice, HomeIntentSuggestionNotice } from "./Conversa
import { MessageList, type ExpertKey, type MessageListMessage } from "./MessageList"
import type { MessageTraceState } from "./useMessageTraces"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
type MessageReaction = "up" | "down"
interface HomeStarterPrompt {
......
......@@ -4,7 +4,7 @@ import { desktopApi } from "../../lib/desktop-api"
import { getTraceDisplayLines, getTraceLineClassName, getTraceStripTitle } from "./messageTraceDisplay"
import type { MessageTraceState } from "./useMessageTraces"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
type MessageReaction = "up" | "down"
export type ExpertKey = "xiaohongshu" | "douyin" | "browser" | "general"
......
......@@ -3,7 +3,7 @@ import type { DesktopApi, WorkspaceSummary } from "@qjclaw/shared-types"
import { EMPTY_SESSION_ID, HOME_CHAT_PROJECT_ID } from "../../lib/constants"
import { resolvePreferredSessionId } from "../../lib/chat-utils"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
interface UseChatSessionsControllerDeps {
desktopApi: DesktopApi
......
......@@ -6,7 +6,7 @@ import { TYPEWRITER_CHARS_PER_FRAME } from "../../lib/constants"
import { canExchangeMessages } from "../../lib/workspace-state"
import type { SmokeStreamSnapshot } from "../smoke/types"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
type SendPhase = "idle" | "preparing" | "streaming" | "finalizing"
interface ActiveStreamState {
requestId: string
......
......@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
import { resolvePreferredSessionId } from "../../lib/chat-utils"
import { HOME_CHAT_PROJECT_ID } from "../../lib/constants"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
export interface PendingHomeIntentSuggestion {
suggestion: ProjectIntentSuggestion
......
......@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
import { HOME_CHAT_PROJECT_ID, HOME_EXPERT_SUGGESTION_PROJECT_IDS } from "../../lib/constants"
import { shouldOfferHomeExpertSwitch } from "../../lib/chat-utils"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
interface ResumePromptOptions {
skipHomeIntentSuggestion?: boolean
......
......@@ -4,7 +4,7 @@ import { Sidebar } from "./Sidebar"
import { ExpertTree, type ExpertCategoryId, type ExpertVisualKey, type SidebarExpertEntry } from "./ExpertTree"
import { SessionList } from "./SessionList"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
type SendPhase = "idle" | "preparing" | "streaming" | "finalizing"
interface AppSidebarProps {
......@@ -70,6 +70,7 @@ export function AppSidebar({
<nav className="nav-list" aria-label="主导航">
{[
{ id: "chat" as const, label: "对话" },
{ id: "tasks" as const, label: "任务面板" },
{ id: "knowledge" as const, label: ui.knowledge },
{ id: "plugins" as const, label: ui.plugins },
{ id: "settings" as const, label: ui.settings }
......
......@@ -104,7 +104,7 @@ function expertMatchesCategory(entry: SidebarExpertEntry, categoryId: ExpertCate
interface ExpertTreeProps {
entries: SidebarExpertEntry[]
expandedCategories: Record<string, boolean>
viewMode: "chat" | "experts" | "plugins" | "settings" | "knowledge"
viewMode: "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
prompt: string
activeProjectId?: string
onToggleCategory(categoryId: string): void
......
......@@ -3,7 +3,7 @@ import type { DesktopApi, ExpertDefinition, WorkspaceSummary } from "@qjclaw/sha
import { EMPTY_SESSION_ID, HOME_CHAT_PROJECT_ID } from "../../lib/constants"
import type { SidebarExpertEntry } from "./ExpertTree"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
interface UseHomeNavigationDeps {
desktopApi: DesktopApi
......
......@@ -14,7 +14,7 @@ import {
buildStandaloneExpertEntries
} from "./expertEntries"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
interface UseSidebarModelOptions {
workspace: WorkspaceSummary | null
......
......@@ -4,7 +4,7 @@ import { HOME_CHAT_PROJECT_ID } from "../../lib/constants"
import type { SubmitPromptOptions } from "../chat/useChatStreamingController"
import { waitForSmokeUiReady } from "./useSmokeActions"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
type ExpertProject = WorkspaceSummary["projects"][number]
type SidebarExpertEntry = {
definition: ExpertDefinition
......
import { useEffect } from "react"
import { smokeEnabled } from "../../lib/desktop-api"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
export async function waitForSmokeUiReady(targetView: ViewMode, timeoutMs = 10000) {
const started = Date.now()
......
......@@ -15,7 +15,7 @@ import type {
import { isMockDesktopApi, smokeEnabled } from "../../lib/desktop-api"
import type { SmokeStreamSnapshot } from "./types"
type ViewMode = "chat" | "experts" | "plugins" | "settings" | "knowledge"
type ViewMode = "chat" | "experts" | "tasks" | "plugins" | "settings" | "knowledge"
export interface SmokeUiSnapshot {
shellReady: boolean
......
import { useEffect, useMemo, useState } from "react"
import type { TaskPanelItem, TaskPanelStatus } from "@qjclaw/shared-types"
import { Panel } from "../../components/ui/Panel"
import { ScrollArea } from "../../components/ui/ScrollArea"
import { StatusChip, type StatusChipTone } from "../../components/ui/StatusChip"
import { getDefaultTaskPanelDate, loadTaskPanelItems } from "./taskPanelData"
const statusLabels: Record<TaskPanelStatus, string> = {
pending: "待处理",
running: "执行中",
completed: "已完成",
failed: "失败"
}
const statusTones: Record<TaskPanelStatus, StatusChipTone> = {
pending: "warning",
running: "info",
completed: "positive",
failed: "warning"
}
function TaskStatus({ item }: { item: TaskPanelItem }) {
return (
<div className="task-panel-status">
<StatusChip tone={statusTones[item.status]}>
<span className={"task-panel-status-dot task-panel-status-dot-" + item.status} aria-hidden="true" />
{statusLabels[item.status]}
</StatusChip>
<span>{item.statusDetail}</span>
</div>
)
}
function TaskArtifacts({ item }: { item: TaskPanelItem }) {
if (!item.artifacts.length) {
return <span className="task-panel-muted">暂无产物</span>
}
return (
<ul className="task-panel-artifact-list" aria-label={item.taskTitle + "产物清单"}>
{item.artifacts.map((artifact) => (
<li key={artifact.id}>
<span className="task-panel-artifact-name">{artifact.name}</span>
{artifact.kind ? <span className="task-panel-artifact-kind">{artifact.kind}</span> : null}
</li>
))}
</ul>
)
}
export function TaskPanelView() {
const [selectedDate, setSelectedDate] = useState(getDefaultTaskPanelDate)
const [items, setItems] = useState<TaskPanelItem[]>([])
const [loading, setLoading] = useState(true)
const [errorText, setErrorText] = useState("")
useEffect(() => {
let active = true
setLoading(true)
setErrorText("")
void loadTaskPanelItems(selectedDate)
.then((nextItems) => {
if (active) {
setItems(nextItems)
}
})
.catch((error: unknown) => {
if (active) {
setItems([])
setErrorText(error instanceof Error ? error.message : "任务列表加载失败")
}
})
.finally(() => {
if (active) {
setLoading(false)
}
})
return () => {
active = false
}
}, [selectedDate])
const completedCount = useMemo(() => items.filter((item) => item.status === "completed").length, [items])
return (
<div className="page-stack task-panel-page-stack">
<Panel className="task-panel-page" bodyClassName="task-panel-body">
<div className="task-panel-header">
<div className="task-panel-title-group">
<h1>任务面板</h1>
<p>{items.length ? `${items.length} 个任务,${completedCount} 个已完成` : "按日期查看专家任务与产物"}</p>
</div>
<label className="task-panel-date-field">
<span>日期</span>
<input
type="date"
value={selectedDate}
onChange={(event) => setSelectedDate(event.currentTarget.value)}
/>
</label>
</div>
<ScrollArea className="scroll-panel task-panel-scroll" aria-busy={loading}>
{loading ? <div className="empty-state task-panel-state">任务列表加载中...</div> : null}
{!loading && errorText ? <div className="notice error task-panel-state" role="alert">{errorText}</div> : null}
{!loading && !errorText && !items.length ? <div className="empty-state task-panel-state">当天暂无任务</div> : null}
{!loading && !errorText && items.length ? (
<div className="task-panel-table" role="table" aria-label="任务面板">
<div className="task-panel-row task-panel-row-head" role="row">
<div role="columnheader">专家</div>
<div role="columnheader">执行状态</div>
<div role="columnheader">产物清单</div>
</div>
{items.map((item) => (
<article key={item.id} className="task-panel-row" role="row">
<div className="task-panel-expert-cell" role="cell">
<strong>{item.expertName}</strong>
<span>{item.taskTitle}</span>
</div>
<div role="cell">
<TaskStatus item={item} />
</div>
<div role="cell">
<TaskArtifacts item={item} />
</div>
</article>
))}
</div>
) : null}
</ScrollArea>
</Panel>
</div>
)
}
import type { TaskPanelItem } from "@qjclaw/shared-types"
function toDateInputValue(date: Date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`
}
function addDays(date: Date, days: number) {
const next = new Date(date)
next.setDate(next.getDate() + days)
return next
}
export function getDefaultTaskPanelDate() {
return toDateInputValue(new Date())
}
export const mockTaskPanelItems: TaskPanelItem[] = [
{
id: "mock-task-content-plan",
date: getDefaultTaskPanelDate(),
expertName: "内容账号规划专家",
taskTitle: "整理本周选题方向与发布节奏",
status: "running",
statusDetail: "正在汇总账号定位、目标人群和栏目节奏",
artifacts: [
{ id: "artifact-content-outline", name: "选题规划草稿.md", kind: "文档" },
{ id: "artifact-content-calendar", name: "发布日历.xlsx", kind: "表格" }
]
},
{
id: "mock-task-zhihu",
date: getDefaultTaskPanelDate(),
expertName: "知乎专家",
taskTitle: "生成知乎回答结构",
status: "completed",
statusDetail: "已完成回答大纲与首版正文",
artifacts: [
{ id: "artifact-zhihu-answer", name: "知乎回答初稿.md", kind: "文档" }
]
},
{
id: "mock-task-leads",
date: getDefaultTaskPanelDate(),
expertName: "平台精准线索专家",
taskTitle: "筛选高意向线索名单",
status: "pending",
statusDetail: "等待线索表上传后开始处理",
artifacts: []
},
{
id: "mock-task-poster",
date: toDateInputValue(addDays(new Date(), -1)),
expertName: "海报专家",
taskTitle: "生成活动海报文案",
status: "failed",
statusDetail: "素材包缺少主视觉图片",
artifacts: []
}
]
export async function loadTaskPanelItems(date: string): Promise<TaskPanelItem[]> {
return mockTaskPanelItems.filter((item) => item.date === date)
}
......@@ -376,6 +376,7 @@ export const mockDesktopApi = {
},
modelConfig: { getSummary: async () => ({ source: "cloud", updatedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(), routingMode: "platform-managed", fallbackMode: "cloud-required", defaultChatModelId: "gpt-5.4-mini", defaultChatModelLabel: "GPT-5.4 Mini", items: [], skillBindings: [], message: "mock" }) },
system: { getSummary: async () => ({ appName: "千匠问天", appVersion: "0.1.0", isPackaged: false, platform: "win32", arch: "x64", appPath: "D:/qjclaw/apps/desktop", resourcesPath: "D:/qjclaw/apps/desktop/dist", userDataPath: "D:/qjclaw/.tmp/user-data", logsPath: "D:/qjclaw/.tmp/logs" }) },
tasks: { listByDate: async () => [] },
chat: {
listSessions: async () => getMockSessions(),
listSessionsByProject: async (projectId: string) => getMockSessions(projectId),
......
......@@ -5,4 +5,5 @@
@import "./styles/settings.css";
@import "./styles/plugins.css";
@import "./styles/knowledge.css";
@import "./styles/tasks.css";
@import "./styles/theme-openclaw.css";
......@@ -399,15 +399,19 @@
padding: 28px;
overflow: hidden;
background:
linear-gradient(135deg, rgba(219, 234, 254, 0.94) 0%, rgba(245, 243, 255, 0.9) 48%, rgba(255, 255, 255, 0.98) 100%),
linear-gradient(180deg, #eff6ff 0%, #ffffff 100%);
radial-gradient(circle at 16% 18%, rgba(90, 176, 255, 0.34), transparent 28%),
radial-gradient(circle at 84% 16%, rgba(255, 255, 255, 0.92), transparent 34%),
radial-gradient(circle at 50% 78%, rgba(190, 228, 255, 0.36), transparent 38%),
linear-gradient(145deg, #dff1ff 0%, #edf7ff 42%, #ffffff 100%);
}
.startup-overlay::before,
.startup-overlay::after {
content: "";
position: absolute;
opacity: 0;
border-radius: 999px;
filter: blur(18px);
opacity: 0.55;
pointer-events: none;
}
......@@ -430,20 +434,14 @@
.startup-overlay-panel {
position: relative;
z-index: 1;
width: min(680px, 100%);
width: min(760px, 100%);
min-height: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 36px;
border: 1px solid rgba(147, 197, 253, 0.38);
border-radius: 30px;
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 30px 70px rgba(37, 99, 235, 0.14);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
padding: 0;
text-align: center;
animation: startup-overlay-enter 420ms ease-out both;
}
......@@ -463,11 +461,11 @@
.startup-overlay-copy h1 {
margin: 0;
font-size: clamp(48px, 5vw, 72px);
font-size: clamp(56px, 6vw, 84px);
line-height: 1.02;
letter-spacing: 0;
letter-spacing: -0.06em;
font-weight: 800;
background-image: linear-gradient(135deg, #1d4ed8 0%, #2563eb 46%, #7c3aed 100%);
background-image: linear-gradient(135deg, #9fd4ff 0%, #bdd7ff 45%, #d8c5ff 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
......@@ -492,16 +490,15 @@
}
.startup-overlay-mark-shell {
width: 64px;
height: 64px;
width: 60px;
height: 60px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 18px;
border: 1px solid rgba(147, 197, 253, 0.42);
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 16px 32px rgba(37, 99, 235, 0.14);
border-radius: 0;
background: transparent;
box-shadow: none;
}
.startup-overlay-logo {
......@@ -515,9 +512,9 @@
margin: 0;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.18em;
letter-spacing: 0.32em;
text-transform: uppercase;
color: #5c78a0;
color: #7ea2d9;
}
.startup-overlay-body {
......@@ -526,64 +523,45 @@
justify-items: center;
gap: 16px;
margin-top: 34px;
padding: 18px;
border-radius: 22px;
border: 1px solid rgba(147, 197, 253, 0.3);
background: rgba(255, 255, 255, 0.74);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);
}
.startup-overlay-progress {
width: 100%;
height: 8px;
height: 7px;
overflow: hidden;
border-radius: 999px;
background: rgba(148, 163, 184, 0.18);
background: rgba(128, 174, 223, 0.18);
}
.startup-overlay-progress span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #2563eb 0%, #7c3aed 100%);
box-shadow: 0 0 18px rgba(37, 99, 235, 0.24);
background: linear-gradient(90deg, #69bcff 0%, #3f8cff 100%);
box-shadow: 0 0 18px rgba(63, 140, 255, 0.28);
transition: width 240ms ease;
}
.startup-overlay-status {
width: 100%;
display: grid;
gap: 8px;
justify-items: center;
}
.startup-overlay-status strong {
color: #0f2f59;
color: #1f426d;
font-size: 16px;
line-height: 1.5;
}
.startup-overlay-status span {
color: #5f7188;
color: #67819f;
font-size: 13px;
line-height: 1.7;
}
.startup-overlay-detail {
width: 100%;
margin: 4px 0 0;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(239, 68, 68, 0.18);
background: rgba(254, 242, 242, 0.84);
color: #9f2f2f;
font-size: 13px;
line-height: 1.6;
}
.startup-overlay-actions {
justify-content: center;
flex-wrap: wrap;
}
@keyframes startup-overlay-enter {
......
.task-panel-page-stack {
height: 100%;
display: flex;
flex-direction: column;
}
.task-panel-page {
flex: 1;
min-height: 0;
padding: 22px;
overflow: hidden;
border-radius: 28px;
border: 1px solid var(--ui-color-border);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 252, 251, 0.96));
box-shadow: var(--ui-shadow-panel);
}
.task-panel-body {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 16px;
min-height: 0;
height: 100%;
padding: 0;
}
.task-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-width: 0;
}
.task-panel-title-group {
display: grid;
gap: 6px;
min-width: 0;
}
.task-panel-title-group h1 {
margin: 0;
color: #1d344f;
font-size: 28px;
line-height: 1.2;
}
.task-panel-title-group p {
margin: 0;
color: #61758f;
font-size: 14px;
line-height: 1.6;
}
.task-panel-date-field {
display: grid;
gap: 6px;
flex: 0 0 auto;
color: #53637f;
font-size: 12px;
font-weight: 700;
}
.task-panel-date-field input {
min-height: 38px;
padding: 0 12px;
border-radius: 12px;
border: 1px solid #d8e1ef;
background: #ffffff;
color: #1f2f49;
font: inherit;
font-size: 13px;
}
.task-panel-scroll {
min-height: 0;
padding-right: 4px;
}
.task-panel-state {
padding: 20px;
}
.task-panel-table {
display: grid;
gap: 10px;
}
.task-panel-row {
display: grid;
grid-template-columns: minmax(190px, 1fr) minmax(190px, 1fr) minmax(220px, 1.15fr);
gap: 16px;
align-items: start;
padding: 16px;
border-radius: 18px;
border: 1px solid rgba(215, 216, 229, 0.96);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 12px 28px rgba(17, 24, 39, 0.04);
}
.task-panel-row-head {
min-height: 44px;
align-items: center;
padding: 10px 16px;
border-radius: 14px;
background: rgba(239, 246, 255, 0.78);
color: #53637f;
font-size: 12px;
font-weight: 800;
}
.task-panel-expert-cell,
.task-panel-status {
display: grid;
gap: 7px;
min-width: 0;
}
.task-panel-expert-cell strong {
color: #1c324d;
font-size: 15px;
line-height: 1.4;
}
.task-panel-expert-cell span,
.task-panel-status > span:not(.status-chip),
.task-panel-muted {
color: #61758f;
font-size: 13px;
line-height: 1.6;
}
.task-panel-status .status-chip {
width: fit-content;
gap: 6px;
}
.task-panel-status-dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: currentColor;
}
.task-panel-status-dot-running {
animation: task-status-pulse 1.2s ease-in-out infinite;
}
.task-panel-status-dot-failed {
color: #b42318;
}
.task-panel-artifact-list {
display: grid;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.task-panel-artifact-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
padding: 8px 10px;
border-radius: 12px;
background: #f8fbff;
border: 1px solid #e3ebf5;
}
.task-panel-artifact-name {
min-width: 0;
color: #1f2f49;
font-size: 13px;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-panel-artifact-kind {
flex: 0 0 auto;
color: #60728c;
font-size: 12px;
font-weight: 700;
}
@keyframes task-status-pulse {
0%, 100% { opacity: 0.42; }
50% { opacity: 1; }
}
@media (max-width: 980px) {
.task-panel-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.task-panel-header {
align-items: stretch;
flex-direction: column;
}
.task-panel-date-field {
width: 100%;
}
}
......@@ -48,7 +48,8 @@
skillsList: "skills:list",
expertsList: "experts:list",
modelConfigGetSummary: "model-config:get-summary",
systemGetSummary: "system:get-summary"
systemGetSummary: "system:get-summary",
tasksListByDate: "tasks:list-by-date"
} as const;
export type GatewayState = "unknown" | "connecting" | "connected" | "disconnected" | "error";
......@@ -77,6 +78,7 @@ export type WorkspaceStartupPhase = "idle" | "syncing-config" | "syncing-project
export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed" | "removed";
export type ExpertEntryMode = "standalone" | "home-chat-shortcut";
export type DailyReportDeliveryState = "draft" | "sent" | "failed";
export type TaskPanelStatus = "pending" | "running" | "completed" | "failed";
export interface WorkspaceWarmupResult {
accepted: boolean;
......@@ -838,6 +840,23 @@ export interface SystemSummary {
logsPath: string;
}
export interface TaskPanelArtifact {
id: string;
name: string;
kind?: string;
url?: string;
}
export interface TaskPanelItem {
id: string;
date: string;
expertName: string;
taskTitle: string;
status: TaskPanelStatus;
statusDetail: string;
artifacts: TaskPanelArtifact[];
}
export interface DesktopApi {
workspace: {
getSummary(): Promise<WorkspaceSummary>;
......@@ -907,6 +926,9 @@ export interface DesktopApi {
system: {
getSummary(): Promise<SystemSummary>;
};
tasks: {
listByDate(date: string): Promise<TaskPanelItem[]>;
};
chat: {
listSessions(): Promise<ProjectSessionSummary[]>;
listSessionsByProject(projectId: string): Promise<ProjectSessionSummary[]>;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment