Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@base-ui/react": "^1.2.0",
"@fontsource-variable/noto-sans": "^5.2.10",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.90.21",
Expand All @@ -34,6 +35,8 @@
"openapi-fetch": "^0.17.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"shadcn": "^3.8.5",
"tailwind-merge": "^3.4.1",
"tailwindcss": "^4.0.6",
Expand All @@ -43,6 +46,7 @@
"devDependencies": {
"@tanstack/devtools-vite": "^0.3.11",
"@tanstack/eslint-config": "^0.3.0",
"@tanstack/react-query-devtools": "^5.99.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
Expand Down
909 changes: 907 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

108 changes: 56 additions & 52 deletions src/components/AgentMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import type { ChatMessage } from '@/lib/adk'

interface AgentMessageProps {
Expand All @@ -8,6 +10,13 @@ interface AgentMessageProps {
export function AgentMessage({ message }: AgentMessageProps) {
const [feedbackGiven, setFeedbackGiven] = useState<'up' | 'down' | null>(null)

const fullText = useMemo(() => {
return message.parts
?.map((p) => p.text ?? '')
.filter(Boolean)
.join('\n')
}, [message.parts])

return (
<div className="flex flex-col items-start w-full">
{/* Agent header */}
Expand All @@ -28,60 +37,56 @@ export function AgentMessage({ message }: AgentMessageProps) {

{/* Message content */}
<div className="w-full text-text-main leading-7 text-sm max-w-none space-y-4">
{/* Render text with line breaks */}
{message.text.split('\n\n').map((paragraph, i) => (
<p key={i} className="whitespace-pre-wrap">
{paragraph}
</p>
))}
{message.parts?.map((part, i) => {
if (part.text) {
return (
<div key={i} className="prose prose-sm max-w-none prose-p:my-2">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{part.text}
</ReactMarkdown>
</div>
)
}

{/* Tool calls */}
{message.toolCalls?.map((tool, i) => (
<div key={i} className="flex items-center gap-2 pl-1">
<span className="material-symbols-outlined text-gray-300 text-xs">
subdirectory_arrow_right
</span>
<div className="tool-badge">
<span className="material-symbols-outlined text-[14px] text-gray-500">
{tool.name.toLowerCase().includes('search')
? 'search'
: tool.name.toLowerCase().includes('verify') ||
tool.name.toLowerCase().includes('check')
? 'shield'
: tool.name.toLowerCase().includes('cofacts')
? 'fact_check'
: 'build'}
</span>
<span>{tool.name}</span>
</div>
</div>
))}
if (part.functionCall) {
const tool = part.functionCall
return (
<div key={i} className="flex items-center gap-2 pl-1">
<span className="material-symbols-outlined text-gray-300 text-xs">
subdirectory_arrow_right
</span>
<div className="tool-badge">
<span className="material-symbols-outlined text-[14px] text-gray-500">
{tool.name?.toLowerCase()?.includes('search')
? 'search'
: tool.name?.toLowerCase()?.includes('verify') ||
tool.name?.toLowerCase()?.includes('check')
? 'shield'
: tool.name?.toLowerCase()?.includes('cofacts')
? 'fact_check'
: 'build'}
</span>
<span>{tool.name}</span>
</div>
</div>
)
}

{/* Streaming indicator */}
{message.isStreaming && (
<p className="flex items-center gap-2 text-gray-500">
正在思考中
<span className="typing-indicator ml-1">
<span />
<span />
<span />
</span>
</p>
)}
return null
})}
</div>

{/* Feedback buttons (only show when not streaming) */}
{!message.isStreaming && message.text && (
<div className="flex items-center gap-3 pt-2 mt-4 border-t border-gray-100">
{!message.isStreaming && fullText && (
<div className="flex items-center gap-3 pt-2 mt-4 border-t border-gray-100 w-full">
<button
onClick={() =>
setFeedbackGiven(feedbackGiven === 'up' ? null : 'up')
}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
feedbackGiven === 'up'
? 'text-primary'
: 'text-gray-400 hover:text-gray-600'
}`}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${feedbackGiven === 'up'
? 'text-primary'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<span className="material-symbols-outlined text-[18px]">
thumb_up
Expand All @@ -91,18 +96,17 @@ export function AgentMessage({ message }: AgentMessageProps) {
onClick={() =>
setFeedbackGiven(feedbackGiven === 'down' ? null : 'down')
}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
feedbackGiven === 'down'
? 'text-destructive'
: 'text-gray-400 hover:text-gray-600'
}`}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${feedbackGiven === 'down'
? 'text-destructive'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<span className="material-symbols-outlined text-[18px]">
thumb_down
</span>
</button>
<button
onClick={() => navigator.clipboard.writeText(message.text)}
onClick={() => navigator.clipboard.writeText(fullText)}
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100 ml-auto"
>
<span className="material-symbols-outlined text-[18px]">
Expand Down
26 changes: 19 additions & 7 deletions src/components/ChatArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,25 @@ export function ChatArea({
</div>
)}

{messages.map((msg) =>
msg.role === 'user' ? (
<UserMessage key={msg.id} message={msg} />
) : (
<AgentMessage key={msg.id} message={msg} />
),
)}
{messages.map((msg) => {
if (msg.role === 'user') {
return msg.author === 'user'
? <UserMessage key={msg.id} message={msg} />
: null // function response, or anything else; render nothing
}
return <AgentMessage key={msg.id} message={msg} />
})}

{
isStreaming && <p className="flex items-center gap-2 text-gray-500">
正在思考中
<span className="typing-indicator ml-1">
<span />
<span />
<span />
</span>
</p>
}

{/* Extra space at the bottom */}
<div className="h-4" />
Expand Down
15 changes: 14 additions & 1 deletion src/components/UserMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import type { ChatMessage } from '@/lib/adk'

interface UserMessageProps {
Expand All @@ -8,7 +10,18 @@ export function UserMessage({ message }: UserMessageProps) {
return (
<div className="flex flex-col items-end">
<div className="bg-bubble-user p-4 rounded-2xl rounded-tr-none max-w-[85%] md:max-w-[70%] text-text-main border border-gray-100 shadow-sm">
<p className="leading-relaxed whitespace-pre-wrap">{message.text}</p>
{message.parts?.map((part, i) => {
if (part.text) {
return (
<div key={i} className="prose prose-sm max-w-none prose-p:my-0">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{part.text}
</ReactMarkdown>
</div>
)
}
return null
})}
</div>
<span className="text-xs text-text-muted mt-1 mr-1">使用者輸入</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import type {
ChatSessionState} from '@/lib/chatCache';
ChatSessionState
} from '@/lib/chatCache';
import {
INITIAL_CHAT_STATE,
abortControllers,
Expand Down Expand Up @@ -78,7 +79,6 @@ export function useChat({ sessionId }: UseChatOptions) {
messages: data.messages,
isStreaming: data.isStreaming,
error,
draftResponse: data.draftResponse,
sources: data.sources,
sendMessage,
resumeRun,
Expand Down
10 changes: 1 addition & 9 deletions src/lib/adk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,9 @@ export type AdkRunPayload = components['schemas']['RunAgentRequest']

export type MessageRole = 'user' | 'model'

export interface ToolCall {
name: string
args?: Record<string, unknown>
}

export interface ChatMessage {
export interface ChatMessage extends AdkContent {
id: string
role: MessageRole
author?: string
text: string
toolCalls?: Array<ToolCall>
isStreaming?: boolean
timestamp?: Date
}
Expand Down
Loading
Loading