feat: add Elec — dark mode multi-model chat UI
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
4
Elec/.gitignore
vendored
Normal file
4
Elec/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
3334
Elec/package-lock.json
generated
Normal file
3334
Elec/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
Elec/package.json
Normal file
30
Elec/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "elec",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"preview": "electron-vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"electron": "^41.3.0",
|
||||||
|
"electron-vite": "^5.0.0",
|
||||||
|
"lucide-react": "^1.11.0",
|
||||||
|
"postcss": "^8.5.12",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.2.4",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^7.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Elec/postcss.config.js
Normal file
5
Elec/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Elec/src/main/index.ts
Normal file
35
Elec/src/main/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { app, BrowserWindow } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 520,
|
||||||
|
height: 900,
|
||||||
|
minWidth: 380,
|
||||||
|
minHeight: 600,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
|
vibrancy: 'hud',
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
win.loadURL(process.env.VITE_DEV_SERVER_URL)
|
||||||
|
} else {
|
||||||
|
win.loadFile(join(__dirname, '../dist/index.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(createWindow)
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
5
Elec/src/preload/index.ts
Normal file
5
Elec/src/preload/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { contextBridge } from 'electron'
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('elecAPI', {
|
||||||
|
platform: process.platform
|
||||||
|
})
|
||||||
12
Elec/src/renderer/index.html
Normal file
12
Elec/src/renderer/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Elec</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
Elec/src/renderer/src/App.tsx
Normal file
18
Elec/src/renderer/src/App.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import ModelSelector from './components/ModelSelector'
|
||||||
|
import ChatArea from './components/ChatArea'
|
||||||
|
import InputArea from './components/InputArea'
|
||||||
|
|
||||||
|
export type Model = 'Omni' | 'Qwen3.5' | 'Deepseek'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [activeModel, setActiveModel] = useState<Model>('Deepseek')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-black">
|
||||||
|
<ModelSelector active={activeModel} onSelect={setActiveModel} />
|
||||||
|
<ChatArea />
|
||||||
|
<InputArea />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
Elec/src/renderer/src/components/ChatArea.tsx
Normal file
16
Elec/src/renderer/src/components/ChatArea.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default function ChatArea() {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-y-auto no-scrollbar relative z-10">
|
||||||
|
<div className="glow-area px-6 py-8 min-h-full flex items-center justify-center">
|
||||||
|
<div className="w-full max-w-sm space-y-4">
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.55)' }}>
|
||||||
|
1. 示例文字:
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.35)' }}>
|
||||||
|
2.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
Elec/src/renderer/src/components/InputArea.tsx
Normal file
14
Elec/src/renderer/src/components/InputArea.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function InputArea() {
|
||||||
|
return (
|
||||||
|
<div className="px-6 pb-8 pt-4 relative z-10">
|
||||||
|
<div className="glow-area">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="输入"
|
||||||
|
className="w-full bg-transparent text-sm outline-none placeholder:text-[rgba(255,255,255,0.25)]"
|
||||||
|
style={{ color: 'rgba(255,255,255,0.5)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
33
Elec/src/renderer/src/components/ModelSelector.tsx
Normal file
33
Elec/src/renderer/src/components/ModelSelector.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Model } from '../App'
|
||||||
|
|
||||||
|
const models: Model[] = ['Omni', 'Qwen3.5', 'Deepseek']
|
||||||
|
|
||||||
|
export default function ModelSelector({
|
||||||
|
active,
|
||||||
|
onSelect
|
||||||
|
}: {
|
||||||
|
active: Model
|
||||||
|
onSelect: (m: Model) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-center gap-8 pt-6 pb-4 z-10 relative"
|
||||||
|
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
{models.map((m) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => onSelect(m)}
|
||||||
|
className="text-sm tracking-wide transition-colors duration-300"
|
||||||
|
style={{
|
||||||
|
color: m === active ? '#ffffff' : 'rgba(255,255,255,0.35)',
|
||||||
|
fontWeight: m === active ? 500 : 400,
|
||||||
|
WebkitAppRegion: 'no-drag'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
Elec/src/renderer/src/index.css
Normal file
38
Elec/src/renderer/src/index.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
color: #e5e5e5;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区的柔光光晕 — 中心微亮,边缘融入纯黑 */
|
||||||
|
.glow-area {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.glow-area::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse 65% 80% at 50% 50%, rgba(255,255,255,0.035) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条隐藏 */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
10
Elec/src/renderer/src/main.tsx
Normal file
10
Elec/src/renderer/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
16
Elec/tsconfig.json
Normal file
16
Elec/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
19
Elec/vite.config.ts
Normal file
19
Elec/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import electron from 'electron-vite/plugin'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
electron([
|
||||||
|
{
|
||||||
|
main: {
|
||||||
|
entry: 'src/main/index.ts',
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
input: 'src/preload/index.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user