feat(todo): 添加待办项关联的创建消息ID并支持消息高亮
- 在待办相关数据结构和存储中新增 created_by_message_id 字段 - 记录待办项创建时对应的消息ID,支持追溯来源 - 在前端待办列表项增加点击事件,点击后滚动并高亮对应消息 - 在消息列表组件中实现高亮动画及自动滚动功能 - 更新相关工具、协议和数据库查询,确保新字段正确传递和存储 - 增加 CSS 动画实现待办对应消息的高亮闪烁效果 - 优化前端状态管理,支持设置与获取高亮消息ID
This commit is contained in:
parent
6a496ce212
commit
4efc8b51e7
@ -76,6 +76,7 @@ impl CommandHandler for ListTodosCommandHandler {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
content: r.content,
|
content: r.content,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
|
created_by_message_id: r.created_by_message_id,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@ -200,6 +200,7 @@ impl BusToolCallEmitter {
|
|||||||
priority: "medium".to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: now + idx as i64,
|
created_at: now + idx as i64,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
|
created_by_message_id: Some(message.id.clone()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -88,6 +88,7 @@ pub struct TodoItemSummary {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
pub created_by_message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -1517,7 +1517,7 @@ impl SessionStore {
|
|||||||
pub fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
|
pub fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at
|
"SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at, created_by_message_id
|
||||||
FROM todos
|
FROM todos
|
||||||
WHERE scope_key = ?1
|
WHERE scope_key = ?1
|
||||||
ORDER BY created_at ASC",
|
ORDER BY created_at ASC",
|
||||||
@ -1534,6 +1534,7 @@ impl SessionStore {
|
|||||||
priority: row.get(6)?,
|
priority: row.get(6)?,
|
||||||
created_at: row.get(7)?,
|
created_at: row.get(7)?,
|
||||||
updated_at: row.get(8)?,
|
updated_at: row.get(8)?,
|
||||||
|
created_by_message_id: row.get(9)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1913,6 +1914,7 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|||||||
priority TEXT NOT NULL DEFAULT 'medium',
|
priority TEXT NOT NULL DEFAULT 'medium',
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL,
|
updated_at INTEGER NOT NULL,
|
||||||
|
created_by_message_id TEXT,
|
||||||
PRIMARY KEY (id, scope_key)
|
PRIMARY KEY (id, scope_key)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1953,6 +1955,7 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|||||||
priority TEXT NOT NULL DEFAULT 'medium',
|
priority TEXT NOT NULL DEFAULT 'medium',
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL,
|
updated_at INTEGER NOT NULL,
|
||||||
|
created_by_message_id TEXT,
|
||||||
PRIMARY KEY (id, scope_key)
|
PRIMARY KEY (id, scope_key)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ pub struct TodoRecord {
|
|||||||
pub priority: String,
|
pub priority: String,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub updated_at: i64,
|
pub updated_at: i64,
|
||||||
|
pub created_by_message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -235,6 +235,7 @@ impl SubAgentEmitter {
|
|||||||
priority: "medium".to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: now + idx as i64,
|
created_at: now + idx as i64,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
|
created_by_message_id: Some(message.id.clone()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -118,6 +118,7 @@ impl Tool for TodoReadTool {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
content: r.content,
|
content: r.content,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
|
created_by_message_id: r.created_by_message_id,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -225,6 +226,7 @@ mod tests {
|
|||||||
priority: "medium".to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: 1000,
|
created_at: 1000,
|
||||||
updated_at: 1000,
|
updated_at: 1000,
|
||||||
|
created_by_message_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +241,7 @@ mod tests {
|
|||||||
id: "a1".to_string(),
|
id: "a1".to_string(),
|
||||||
content: "任务A".to_string(),
|
content: "任务A".to_string(),
|
||||||
status: "pending".to_string(),
|
status: "pending".to_string(),
|
||||||
|
created_by_message_id: None,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -314,6 +317,7 @@ mod tests {
|
|||||||
id: "m1".to_string(),
|
id: "m1".to_string(),
|
||||||
content: "主会话任务".to_string(),
|
content: "主会话任务".to_string(),
|
||||||
status: "pending".to_string(),
|
status: "pending".to_string(),
|
||||||
|
created_by_message_id: None,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ pub(crate) struct TodoItem {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
pub created_by_message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 工具完整返回
|
/// 工具完整返回
|
||||||
@ -143,7 +144,10 @@ impl Tool for TodoWriteTool {
|
|||||||
None => return Ok(error_result("todo_write requires session_id or topic_id in tool context")),
|
None => return Ok(error_result("todo_write requires session_id or topic_id in tool context")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 解析入参
|
// 2. 提取当前消息 ID(用于记录待办的创建来源)
|
||||||
|
let message_id = context.message_id.clone();
|
||||||
|
|
||||||
|
// 3. 解析入参
|
||||||
let todos_array = match args.get("todos").and_then(|v| v.as_array()) {
|
let todos_array = match args.get("todos").and_then(|v| v.as_array()) {
|
||||||
Some(arr) => arr,
|
Some(arr) => arr,
|
||||||
None => return Ok(error_result("Missing required parameter: todos (must be an array)")),
|
None => return Ok(error_result("Missing required parameter: todos (must be an array)")),
|
||||||
@ -219,6 +223,7 @@ impl Tool for TodoWriteTool {
|
|||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
status: new_status.as_str().to_string(),
|
status: new_status.as_str().to_string(),
|
||||||
|
created_by_message_id: message_id.clone(),
|
||||||
});
|
});
|
||||||
} else if merge_mode {
|
} else if merge_mode {
|
||||||
// merge 模式:id 不匹配,尝试 content fallback
|
// merge 模式:id 不匹配,尝试 content fallback
|
||||||
@ -238,6 +243,7 @@ impl Tool for TodoWriteTool {
|
|||||||
id: old_item.id.clone(),
|
id: old_item.id.clone(),
|
||||||
content,
|
content,
|
||||||
status: new_status.as_str().to_string(),
|
status: new_status.as_str().to_string(),
|
||||||
|
created_by_message_id: message_id.clone(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 全新项
|
// 全新项
|
||||||
@ -245,6 +251,7 @@ impl Tool for TodoWriteTool {
|
|||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
status: new_status.as_str().to_string(),
|
status: new_status.as_str().to_string(),
|
||||||
|
created_by_message_id: message_id.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -253,6 +260,7 @@ impl Tool for TodoWriteTool {
|
|||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
status: new_status.as_str().to_string(),
|
status: new_status.as_str().to_string(),
|
||||||
|
created_by_message_id: message_id.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { ChannelSelector } from './components/Header/ChannelSelector'
|
|||||||
import { SessionSelector } from './components/Header/SessionSelector'
|
import { SessionSelector } from './components/Header/SessionSelector'
|
||||||
import { useWebSocket } from './hooks/useWebSocket'
|
import { useWebSocket } from './hooks/useWebSocket'
|
||||||
import { useChat } from './hooks/useChat'
|
import { useChat } from './hooks/useChat'
|
||||||
import type { ChatMessage, Command, Attachment, SchedulerJobSessionLookup } from './types/protocol'
|
import type { ChatMessage, Command, Attachment, SchedulerJobSessionLookup, TodoItemSummary } from './types/protocol'
|
||||||
|
|
||||||
function getInitialSettings(): GatewaySettings {
|
function getInitialSettings(): GatewaySettings {
|
||||||
return getGatewaySettings()
|
return getGatewaySettings()
|
||||||
@ -54,6 +54,9 @@ function App() {
|
|||||||
setTodos,
|
setTodos,
|
||||||
requestTodoList,
|
requestTodoList,
|
||||||
requestSubAgentTodoList,
|
requestSubAgentTodoList,
|
||||||
|
// 高亮消息
|
||||||
|
highlightedMessageId,
|
||||||
|
setHighlightedMessageId,
|
||||||
// 定时任务
|
// 定时任务
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
@ -396,6 +399,17 @@ function App() {
|
|||||||
: requestTodoList()
|
: requestTodoList()
|
||||||
}, [subAgentView, requestTodoList, requestSubAgentTodoList])
|
}, [subAgentView, requestTodoList, requestSubAgentTodoList])
|
||||||
|
|
||||||
|
// 点击待办项后滚动到对应消息
|
||||||
|
const handleTodoClick = useCallback((todo: TodoItemSummary) => {
|
||||||
|
// 直接使用后端返回的 created_by_message_id
|
||||||
|
if (todo.created_by_message_id) {
|
||||||
|
setHighlightedMessageId(todo.created_by_message_id)
|
||||||
|
} else {
|
||||||
|
// 如果消息 ID 不存在(旧数据),给出友好提示
|
||||||
|
alert('该待办的完成记录无法定位,可能是历史数据')
|
||||||
|
}
|
||||||
|
}, [setHighlightedMessageId])
|
||||||
|
|
||||||
const handleRefreshSchedulerJobs = useCallback(() => {
|
const handleRefreshSchedulerJobs = useCallback(() => {
|
||||||
const cmd = requestSchedulerJobList()
|
const cmd = requestSchedulerJobList()
|
||||||
handleCommand(cmd)
|
handleCommand(cmd)
|
||||||
@ -694,11 +708,13 @@ function App() {
|
|||||||
onStop={handleStopExecution}
|
onStop={handleStopExecution}
|
||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
viewKey={viewKey}
|
viewKey={viewKey}
|
||||||
|
highlightedMessageId={highlightedMessageId}
|
||||||
todoPanel={
|
todoPanel={
|
||||||
<TodoPanel
|
<TodoPanel
|
||||||
todos={todos}
|
todos={todos}
|
||||||
requestTodoList={refreshTodoList}
|
requestTodoList={refreshTodoList}
|
||||||
sendCommand={sendMemoryCommand}
|
sendCommand={sendMemoryCommand}
|
||||||
|
onTodoClick={handleTodoClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,6 +15,8 @@ interface ChatContainerProps {
|
|||||||
todoPanel?: React.ReactNode
|
todoPanel?: React.ReactNode
|
||||||
/** 视图标识,用于保存/恢复滚动位置 */
|
/** 视图标识,用于保存/恢复滚动位置 */
|
||||||
viewKey?: string
|
viewKey?: string
|
||||||
|
/** 高亮的消息 ID */
|
||||||
|
highlightedMessageId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatContainer({
|
export function ChatContainer({
|
||||||
@ -28,11 +30,12 @@ export function ChatContainer({
|
|||||||
showThinking = true,
|
showThinking = true,
|
||||||
todoPanel,
|
todoPanel,
|
||||||
viewKey,
|
viewKey,
|
||||||
|
highlightedMessageId,
|
||||||
}: ChatContainerProps) {
|
}: ChatContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col relative">
|
<div className="flex h-full flex-col relative">
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} />
|
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} highlightedMessageId={highlightedMessageId} />
|
||||||
{todoPanel}
|
{todoPanel}
|
||||||
</div>
|
</div>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
|
|||||||
@ -647,7 +647,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex gap-4 ${isUser ? 'flex-row-reverse' : 'flex-row'} animate-slide-in group`}>
|
<div data-message-id={message.id} className={`flex gap-4 ${isUser ? 'flex-row-reverse' : 'flex-row'} animate-slide-in group`}>
|
||||||
<div
|
<div
|
||||||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${getAvatarStyles()} shadow-lg`}
|
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${getAvatarStyles()} shadow-lg`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -9,9 +9,11 @@ interface MessageListProps {
|
|||||||
showThinking?: boolean
|
showThinking?: boolean
|
||||||
/** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */
|
/** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */
|
||||||
viewKey?: string
|
viewKey?: string
|
||||||
|
/** 高亮的消息 ID,点击待办项后滚动并高亮显示 */
|
||||||
|
highlightedMessageId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageList({ messages, onNavigateToSubAgent, showThinking = true, viewKey }: MessageListProps) {
|
export function MessageList({ messages, onNavigateToSubAgent, showThinking = true, viewKey, highlightedMessageId }: MessageListProps) {
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const isAtBottomRef = useRef(true)
|
const isAtBottomRef = useRef(true)
|
||||||
@ -117,6 +119,28 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// ---- highlight and scroll to todo message ----
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!highlightedMessageId) return
|
||||||
|
|
||||||
|
const container = containerRef.current
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 查找目标消息元素
|
||||||
|
const targetElement = container.querySelector(`[data-message-id="${highlightedMessageId}"]`)
|
||||||
|
if (!targetElement) return
|
||||||
|
|
||||||
|
// 滚动到目标位置
|
||||||
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
|
||||||
|
// 添加高亮样式
|
||||||
|
targetElement.classList.add('todo-highlight')
|
||||||
|
setTimeout(() => {
|
||||||
|
targetElement.classList.remove('todo-highlight')
|
||||||
|
}, 2000)
|
||||||
|
}, [highlightedMessageId])
|
||||||
|
|
||||||
// ---- empty state ----
|
// ---- empty state ----
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ interface TodoPanelProps {
|
|||||||
todos: TodoItemSummary[]
|
todos: TodoItemSummary[]
|
||||||
requestTodoList: () => Command
|
requestTodoList: () => Command
|
||||||
sendCommand: (cmd: Command) => void
|
sendCommand: (cmd: Command) => void
|
||||||
|
onTodoClick?: (todo: TodoItemSummary) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── status config ────────────────────────────────────── */
|
/* ── status config ────────────────────────────────────── */
|
||||||
@ -64,7 +65,7 @@ function savePos(pos: { x: number; y: number }) {
|
|||||||
|
|
||||||
/* ── TodoPanel ────────────────────────────────────────── */
|
/* ── TodoPanel ────────────────────────────────────────── */
|
||||||
|
|
||||||
export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProps) {
|
export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }: TodoPanelProps) {
|
||||||
const [expanded, setExpanded] = useState(() => {
|
const [expanded, setExpanded] = useState(() => {
|
||||||
try { return localStorage.getItem('picobot-todo-expanded') === 'true' } catch { return false }
|
try { return localStorage.getItem('picobot-todo-expanded') === 'true' } catch { return false }
|
||||||
})
|
})
|
||||||
@ -249,12 +250,16 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
<div className={`todo-group-body ${isCollapsed ? 'todo-group-body-closed' : 'todo-group-body-open'}`}>
|
<div className={`todo-group-body ${isCollapsed ? 'todo-group-body-closed' : 'todo-group-body-open'}`}>
|
||||||
<div className="ml-[7px] border-l-2 border-[var(--border-color)]/60 pl-3 mt-1.5 space-y-0.5">
|
<div className="ml-[7px] border-l-2 border-[var(--border-color)]/60 pl-3 mt-1.5 space-y-0.5">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div key={item.id} className="group/item py-1.5 px-2 -mx-2 rounded-md transition-colors duration-150 hover:bg-[var(--overlay-hover)] flex items-start gap-1.5">
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => onTodoClick?.(item)}
|
||||||
|
className="group/item w-full text-left py-1.5 px-2 -mx-2 rounded-md transition-colors duration-150 hover:bg-[var(--overlay-hover)] flex items-start gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0 mt-1.5`} />
|
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0 mt-1.5`} />
|
||||||
<span className="text-[13px] leading-relaxed text-[var(--text-primary)]/85 group-hover/item:text-[var(--text-primary)] transition-colors break-words">
|
<span className="text-[13px] leading-relaxed text-[var(--text-primary)]/85 group-hover/item:text-[var(--text-primary)] transition-colors break-words">
|
||||||
{item.content}
|
{item.content}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -106,6 +106,10 @@ interface UseChatReturn {
|
|||||||
requestTodoList: () => Command
|
requestTodoList: () => Command
|
||||||
requestSubAgentTodoList: (subTaskId: string) => Command
|
requestSubAgentTodoList: (subTaskId: string) => Command
|
||||||
|
|
||||||
|
// 高亮消息 ID(点击待办后滚动到对应消息)
|
||||||
|
highlightedMessageId: string | null
|
||||||
|
setHighlightedMessageId: Dispatch<SetStateAction<string | null>>
|
||||||
|
|
||||||
// 定时任务状态
|
// 定时任务状态
|
||||||
schedulerJobs: SchedulerJobSummary[]
|
schedulerJobs: SchedulerJobSummary[]
|
||||||
sidebarTab: 'topics' | 'scheduler'
|
sidebarTab: 'topics' | 'scheduler'
|
||||||
@ -156,6 +160,7 @@ export function useChat(): UseChatReturn {
|
|||||||
const [memories, setMemories] = useState<MemorySummary[]>([])
|
const [memories, setMemories] = useState<MemorySummary[]>([])
|
||||||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||||||
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
||||||
|
const [highlightedMessageId, setHighlightedMessageId] = useState<string | null>(null)
|
||||||
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
||||||
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
||||||
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
||||||
@ -1101,6 +1106,8 @@ export function useChat(): UseChatReturn {
|
|||||||
setTodos,
|
setTodos,
|
||||||
requestTodoList,
|
requestTodoList,
|
||||||
requestSubAgentTodoList,
|
requestSubAgentTodoList,
|
||||||
|
highlightedMessageId,
|
||||||
|
setHighlightedMessageId,
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
setSidebarTab,
|
setSidebarTab,
|
||||||
|
|||||||
@ -294,10 +294,25 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes todo-highlight-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(0, 240, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animate-todo-card-in { animation: todo-card-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
.animate-todo-card-in { animation: todo-card-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||||
.animate-todo-item-in { animation: todo-item-in 0.2s ease-out forwards; }
|
.animate-todo-item-in { animation: todo-item-in 0.2s ease-out forwards; }
|
||||||
.animate-todo-ring-pulse { animation: todo-ring-pulse 2s ease-in-out infinite; }
|
.animate-todo-ring-pulse { animation: todo-ring-pulse 2s ease-in-out infinite; }
|
||||||
|
|
||||||
|
/* 待办点击高亮效果 */
|
||||||
|
.todo-highlight {
|
||||||
|
animation: todo-highlight-pulse 1s ease-in-out 2;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 分组折叠内容展开/收起 */
|
/* 分组折叠内容展开/收起 */
|
||||||
.todo-group-body {
|
.todo-group-body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -210,6 +210,7 @@ export interface TodoItemSummary {
|
|||||||
priority: string
|
priority: string
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
|
created_by_message_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TodoList {
|
export interface TodoList {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user