refactor(home): 将首页改造成opencode服务入口页面

- 移除ChatGPT风格对话界面相关代码
- 添加在首页自动启动/停止opencode serve的IPC调用
- 首页使用webview加载opencode服务器界面
This commit is contained in:
2026-03-10 16:20:32 +08:00
parent de4c101b36
commit 2503d8be64
9 changed files with 123 additions and 286 deletions

View File

@@ -1,5 +1,62 @@
import { ChatLayout } from './components/ChatLayout'
import { useRef, useEffect, useState } from 'react'
export const HomePage = () => {
return <ChatLayout />
const webviewRef = useRef<HTMLWebViewElement>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const startOpencodeServer = async () => {
try {
const result = await window.electronAPI?.opencodeStartServer()
if (!result?.success) {
setError(result?.error || 'Failed to start opencode server')
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
}
}
startOpencodeServer()
return () => {
window.electronAPI?.opencodeStopServer()
}
}, [])
useEffect(() => {
const webview = webviewRef.current
if (!webview) return
webview.addEventListener('did-fail-load', (e) => {
console.error('[HomePage] Failed to load:', e)
setIsLoading(false)
})
webview.addEventListener('did-finish-load', () => {
setIsLoading(false)
})
}, [])
return (
<div className="h-full w-full relative">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-10">
<div className="text-gray-500 dark:text-gray-400"> opencode ...</div>
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-10">
<div className="text-red-500">: {error}</div>
</div>
)}
<webview
ref={webviewRef}
src="http://127.0.0.1:4096"
className="w-full h-full"
nodeintegration={false}
webpreferences="contextIsolation=yes"
/>
</div>
)
}

View File

@@ -1,120 +0,0 @@
import { Send } from 'lucide-react'
import { useChatStore, type ChatMessage } from '@/stores/chatStore'
import { useState, useRef, useEffect } from 'react'
const MessageItem = ({ message }: { message: ChatMessage }) => {
const isUser = message.role === 'user'
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
<div
className={`flex gap-3 max-w-[80%] ${
isUser ? 'flex-row-reverse' : 'flex-row'
}`}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
isUser ? 'bg-gray-600' : 'bg-green-600'
}`}>
{isUser ? (
<span className="text-white text-sm">👤</span>
) : (
<span className="text-white text-sm">🤖</span>
)}
</div>
<div
className={`px-4 py-2.5 rounded-lg text-sm leading-relaxed whitespace-pre-wrap ${
isUser
? 'bg-[#343541] text-white'
: 'bg-transparent text-gray-100'
}`}
>
{message.content}
</div>
</div>
</div>
)
}
export const ChatContent = () => {
const { currentChatId, chats, addMessage, updateChatTitle } = useChatStore()
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
const currentChat = chats.find((c) => c.id === currentChatId)
const messages = currentChat?.messages ?? []
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages.length])
const handleSend = () => {
if (!input.trim() || !currentChatId) return
const userMessage: ChatMessage = {
id: Math.random().toString(36).substring(2, 15),
role: 'user',
content: input.trim(),
timestamp: Date.now(),
}
addMessage(currentChatId, userMessage)
if (messages.length === 0) {
const title = input.trim().slice(0, 20) + (input.trim().length > 20 ? '...' : '')
updateChatTitle(currentChatId, title)
}
setInput('')
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
return (
<div className="flex-1 flex flex-col h-full bg-[#343541]">
<div className="flex-1 overflow-y-auto px-4 py-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<h1 className="text-2xl font-light text-gray-400">
</h1>
</div>
) : (
<div className="max-w-3xl mx-auto pt-4">
{messages.map((msg) => (
<MessageItem key={msg.id} message={msg} />
))}
<div ref={messagesEndRef} />
</div>
)}
</div>
<div className="px-4 pb-4">
<div className="max-w-3xl mx-auto flex gap-2 items-end">
<div className="flex-1">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="发送消息..."
rows={1}
className="w-full bg-[#40414f] text-white rounded-lg px-4 py-3 resize-none focus:outline-none focus:ring-2 focus:ring-green-500 placeholder-gray-400 text-sm"
style={{ minHeight: '48px', maxHeight: '200px' }}
/>
</div>
<button
onClick={handleSend}
disabled={!input.trim()}
className="px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors shrink-0"
>
<Send size={18} className="text-white" />
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,11 +0,0 @@
import { ChatSidebar } from './ChatSidebar'
import { ChatContent } from './ChatContent'
export const ChatLayout = () => {
return (
<div className="flex h-full w-full overflow-hidden">
<ChatSidebar />
<ChatContent />
</div>
)
}

View File

@@ -1,53 +0,0 @@
import { Plus, Trash2 } from 'lucide-react'
import { useChatStore } from '@/stores/chatStore'
export const ChatSidebar = () => {
const { chats, currentChatId, createChat, deleteChat, selectChat } = useChatStore()
const handleNewChat = () => {
createChat()
}
return (
<div className="w-[260px] h-full flex flex-col bg-[#262626] dark:bg-[#262626]">
<button
onClick={handleNewChat}
className="flex items-center gap-2 px-3 py-2.5 mx-2 mt-2 rounded-md border border-gray-600 hover:bg-gray-700 transition-colors text-sm"
>
<Plus size={16} />
<span></span>
</button>
<div className="flex-1 overflow-y-auto mt-3 px-2">
{chats.map((chat) => (
<div
key={chat.id}
onClick={() => selectChat(chat.id)}
className={`group flex items-center justify-between px-3 py-2 rounded-md cursor-pointer text-sm mb-1 transition-colors ${
currentChatId === chat.id
? 'bg-[#343541] text-white'
: 'text-gray-300 hover:bg-[#343541]/50'
}`}
>
<span className="truncate flex-1">{chat.title}</span>
<button
onClick={(e) => {
e.stopPropagation()
deleteChat(chat.id)
}}
className="opacity-0 group-hover:opacity-100 hover:text-red-400 transition-opacity p-1"
>
<Trash2 size={14} />
</button>
</div>
))}
{chats.length === 0 && (
<div className="text-gray-500 text-sm text-center py-8">
</div>
)}
</div>
</div>
)
}

View File

@@ -7,5 +7,3 @@ export default createFrontendModule(HOME_MODULE, {
icon: Home,
component: HomePage,
})
export { HomePage }

View File

@@ -1,98 +0,0 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}
export interface Chat {
id: string
title: string
messages: ChatMessage[]
createdAt: number
updatedAt: number
}
interface ChatState {
chats: Chat[]
currentChatId: string | null
createChat: () => string
deleteChat: (id: string) => void
selectChat: (id: string) => void
updateChatTitle: (id: string, title: string) => void
addMessage: (chatId: string, message: ChatMessage) => void
getCurrentChat: () => Chat | null
}
const generateId = () => Math.random().toString(36).substring(2, 15)
export const useChatStore = create<ChatState>()(
persist(
(set, get) => ({
chats: [],
currentChatId: null,
createChat: () => {
const id = generateId()
const now = Date.now()
const newChat: Chat = {
id,
title: '新对话',
messages: [],
createdAt: now,
updatedAt: now,
}
set((state) => ({
chats: [newChat, ...state.chats],
currentChatId: id,
}))
return id
},
deleteChat: (id) => {
set((state) => {
const newChats = state.chats.filter((c) => c.id !== id)
const newCurrentId = state.currentChatId === id
? (newChats[0]?.id ?? null)
: state.currentChatId
return { chats: newChats, currentChatId: newCurrentId }
})
},
selectChat: (id) => {
set({ currentChatId: id })
},
updateChatTitle: (id, title) => {
set((state) => ({
chats: state.chats.map((c) =>
c.id === id ? { ...c, title, updatedAt: Date.now() } : c
),
}))
},
addMessage: (chatId, message) => {
set((state) => ({
chats: state.chats.map((c) =>
c.id === chatId
? { ...c, messages: [...c.messages, message], updatedAt: Date.now() }
: c
),
}))
},
getCurrentChat: () => {
const state = get()
return state.chats.find((c) => c.id === state.currentChatId) ?? null
},
}),
{
name: 'chat-storage',
}
)
)

View File

@@ -38,6 +38,8 @@ export interface ElectronAPI {
error?: string
canceled?: boolean
}>
opencodeStartServer: () => Promise<{ success: boolean; port?: number; error?: string }>
opencodeStopServer: () => Promise<{ success: boolean; error?: string }>
}
declare global {