feat: add Elec — dark mode multi-model chat UI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fyah
2026-05-09 11:14:28 +08:00
parent e9daabb821
commit 0ee021e613
15 changed files with 3589 additions and 0 deletions

4
Elec/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
out/
dist/
.DS_Store

3334
Elec/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
Elec/package.json Normal file
View 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
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
autoprefixer: {}
}
}

35
Elec/src/main/index.ts Normal file
View 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()
})

View File

@@ -0,0 +1,5 @@
import { contextBridge } from 'electron'
contextBridge.exposeInMainWorld('elecAPI', {
platform: process.platform
})

View 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>

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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;
}

View 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
View 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
View 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',
},
},
]),
],
})