chore: initial backup
16
.codegraph/.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# CodeGraph data files
|
||||||
|
# These are local to each machine and should not be committed
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Hook markers
|
||||||
|
.dirty
|
||||||
6
.codegraph/daemon.pid
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"pid": 30162,
|
||||||
|
"version": "0.9.7",
|
||||||
|
"socketPath": "/Users/wang/agent_dev/wechat_ai/.codegraph/daemon.sock",
|
||||||
|
"startedAt": 1781241939180
|
||||||
|
}
|
||||||
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/gen/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
12
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>AI微信分身助手</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2761
package-lock.json
generated
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "wechat-ai-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 127.0.0.1",
|
||||||
|
"build": "vite build",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"preview": "vite preview --host 127.0.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"lucide-react": "^1.17.0",
|
||||||
|
"postcss": "^8.5.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.11.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
||||||
4940
src-tauri/Cargo.lock
generated
Normal file
25
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["大森"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = ""
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.6.2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
tauri = { version = "2.11.2", features = [] }
|
||||||
|
tauri-plugin-log = "2"
|
||||||
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
11
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
16
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.handle().plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
app_lib::run();
|
||||||
|
}
|
||||||
40
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
|
"productName": "微信A助手",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.tauri.dev",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://127.0.0.1:5173",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "微信AI助手",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"android": {
|
||||||
|
"debugApplicationIdSuffix": ".debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/App.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import MainShell from "./components/MainShell";
|
||||||
|
import SecondaryWindow from "./windows/SecondaryWindow";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [theme, setTheme] = useState(() => localStorage.getItem("theme") || "light");
|
||||||
|
const [activeTab, setActiveTab] = useState("clone");
|
||||||
|
const [activeKnowledge, setActiveKnowledge] = useState("全部");
|
||||||
|
const [activeSkill, setActiveSkill] = useState("全部");
|
||||||
|
const [settingSection, setSettingSection] = useState("基础配置");
|
||||||
|
const [route, setRoute] = useState(() => window.location.hash.replace("#", "") || "/");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onHashChange = () => setRoute(window.location.hash.replace("#", "") || "/");
|
||||||
|
window.addEventListener("hashchange", onHashChange);
|
||||||
|
return () => window.removeEventListener("hashchange", onHashChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isWindowRoute = route.startsWith("/window/");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh px-4 py-4 text-text sm:px-6">
|
||||||
|
{isWindowRoute ? (
|
||||||
|
<SecondaryWindow
|
||||||
|
route={route}
|
||||||
|
theme={theme}
|
||||||
|
setTheme={setTheme}
|
||||||
|
settingSection={settingSection}
|
||||||
|
setSettingSection={setSettingSection}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MainShell
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
theme={theme}
|
||||||
|
setTheme={setTheme}
|
||||||
|
activeKnowledge={activeKnowledge}
|
||||||
|
setActiveKnowledge={setActiveKnowledge}
|
||||||
|
activeSkill={activeSkill}
|
||||||
|
setActiveSkill={setActiveSkill}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
21
src/components/BottomTabs.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { tabs } from "../data/mockData";
|
||||||
|
import IconBox from "./IconBox";
|
||||||
|
|
||||||
|
export default function BottomTabs({ activeTab, setActiveTab }) {
|
||||||
|
return (
|
||||||
|
<nav className="grid grid-cols-4 border-t border-line px-2 py-2">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={`relative flex flex-col items-center gap-1 rounded-sm px-2 py-2 text-[12px] font-bold transition ${
|
||||||
|
activeTab === tab.id ? " text-primary" : "text-muted hover:bg-surface2"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
>
|
||||||
|
<IconBox icon={tab.icon} small active={activeTab === tab.id} />
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/DraggableFab.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
export default function DraggableFab({ onClick, ariaLabel }) {
|
||||||
|
const dragRef = useRef({ startY: 0, startTop: 0, dragged: false, dragging: false });
|
||||||
|
const [top, setTop] = useState(() => Math.max(24, window.innerHeight - 252));
|
||||||
|
|
||||||
|
const clampTop = (value) => Math.min(Math.max(value, 24), window.innerHeight - 180);
|
||||||
|
|
||||||
|
function handlePointerDown(event) {
|
||||||
|
dragRef.current = { startY: event.clientY, startTop: top, dragged: false, dragging: true };
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(event) {
|
||||||
|
if (!dragRef.current.dragging) return;
|
||||||
|
|
||||||
|
const deltaY = event.clientY - dragRef.current.startY;
|
||||||
|
|
||||||
|
if (Math.abs(deltaY) > 3) {
|
||||||
|
dragRef.current.dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTop(clampTop(dragRef.current.startTop + deltaY));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(event) {
|
||||||
|
dragRef.current.dragging = false;
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
if (dragRef.current.dragged) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="fab touch-none cursor-grab active:cursor-grabbing"
|
||||||
|
style={{ top, bottom: "auto" }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<Plus size={30} strokeWidth={2.4} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/components/EditorBlock.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import StatusPill from "./StatusPill";
|
||||||
|
|
||||||
|
export default function EditorBlock({ title, text, locked, fill = false }) {
|
||||||
|
return (
|
||||||
|
<div className={fill ? "flex min-h-0 flex-1 flex-col" : undefined}>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<span className="text-[12px] font-black text-muted">{title}</span>
|
||||||
|
{locked ? <StatusPill tone="muted" label="不可修改" /> : null}
|
||||||
|
</div>
|
||||||
|
<textarea className={`editor ${fill ? "flex-1" : ""}`} defaultValue={text} disabled={locked} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/components/Field.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function Field({ label, value, secret, disabled = false, readOnly = false }) {
|
||||||
|
return (
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-2 block text-[12px] font-black text-muted">{label}</span>
|
||||||
|
<input className="input-like" defaultValue={value} disabled={disabled} readOnly={readOnly} type={secret ? "password" : "text"} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/IconBox.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function IconBox({ icon: Icon, label, small = false, strong = false, active = false }) {
|
||||||
|
return (
|
||||||
|
<span className={`${small ? "icon-box-sm" : "icon-box"} ${strong ? "icon-box-strong" : ""} ${active ? "icon-active" : ""}`}>
|
||||||
|
{Icon ? <Icon size={small ? 14 : 18} strokeWidth={2.5} /> : label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/MainShell.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { tabs } from "../data/mockData";
|
||||||
|
import BottomTabs from "./BottomTabs";
|
||||||
|
import TitleBar from "./TitleBar";
|
||||||
|
import ClonePage from "../pages/ClonePage";
|
||||||
|
import DistillPage from "../pages/DistillPage";
|
||||||
|
import KnowledgePage from "../pages/KnowledgePage";
|
||||||
|
import SkillsPage from "../pages/SkillsPage";
|
||||||
|
|
||||||
|
export default function MainShell(props) {
|
||||||
|
const activeTitle = tabs.find((tab) => tab.id === props.activeTab)?.label ?? "AI微信分身助手";
|
||||||
|
const content = {
|
||||||
|
clone: <ClonePage />,
|
||||||
|
knowledge: <KnowledgePage active={props.activeKnowledge} setActive={props.setActiveKnowledge} />,
|
||||||
|
skills: <SkillsPage active={props.activeSkill} setActive={props.setActiveSkill} />,
|
||||||
|
distill: <DistillPage />,
|
||||||
|
}[props.activeTab];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-window mx-auto flex min-h-[calc(100dvh-32px)] w-full max-w-[750px] flex-col overflow-hidden rounded-[10px] border border-line shadow-panel">
|
||||||
|
<TitleBar title={activeTitle} theme={props.theme} setTheme={props.setTheme} />
|
||||||
|
<main className="relative flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-5">{content}</main>
|
||||||
|
<BottomTabs activeTab={props.activeTab} setActiveTab={props.setActiveTab} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/components/SectionHeading.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function SectionHeading({ title, desc }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[22px] font-extrabold">{title}</h2>
|
||||||
|
<p className="mt-2 text-[13px] leading-6 text-muted">{desc}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/Segmented.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export default function Segmented({ items, active, onChange }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 overflow-x-auto rounded-[8px] border border-line bg-surface p-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
className={`shrink-0 rounded-full px-8 py-1.5 text-[13px] transition ${
|
||||||
|
active === item ? "bg-primary text-white shadow-[0_10px_24px_rgba(7,193,96,.22)]" : "text-muted hover:bg-surface2"
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/components/StatusPill.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function StatusPill({ label, tone = "success" }) {
|
||||||
|
const toneClass = {
|
||||||
|
success: "status-success",
|
||||||
|
warning: "status-warning",
|
||||||
|
muted: "status-muted",
|
||||||
|
}[tone] ?? "status-success";
|
||||||
|
|
||||||
|
return <span className={`status ${toneClass}`}>{label}</span>;
|
||||||
|
}
|
||||||
29
src/components/Terminal.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export default function Terminal({ title, lines, tall = false }) {
|
||||||
|
const levelClass = {
|
||||||
|
info: "terminal-line-info",
|
||||||
|
error: "terminal-line-error",
|
||||||
|
warning: "terminal-line-warning",
|
||||||
|
waring: "terminal-line-warning",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`terminal ${tall ? "basis-[650px]" : "basis-[250px]"} flex min-h-0 flex-1 flex-col`}>
|
||||||
|
<div className="mb-3 flex shrink-0 items-center justify-between">
|
||||||
|
<h3 className="text-[13px] font-black text-[#d7ffe4]">{title}</h3>
|
||||||
|
<span className="cursor-pointer text-[11px] font-bold text-[#7ea58b]">清理日志</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto font-mono text-[12px] leading-5">
|
||||||
|
{lines.length ? (
|
||||||
|
lines.map((line, index) => {
|
||||||
|
const level = typeof line === "string" ? "info" : line.level;
|
||||||
|
const message = typeof line === "string" ? line : line.message;
|
||||||
|
|
||||||
|
return <p key={`${message}-${index}`} className={levelClass[level] ?? levelClass.info}>{message}</p>;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="terminal-line-info">引擎未启动</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/components/ThemeSwitch.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
|
export default function ThemeSwitch({ theme, setTheme }) {
|
||||||
|
const dark = theme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="icon-btn" onClick={() => setTheme(dark ? "light" : "dark")} aria-label="切换主题">
|
||||||
|
{dark ? <Moon size={16} strokeWidth={2.6} /> : <Sun size={16} strokeWidth={2.6} />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/components/TitleBar.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { LogOut, RotateCcw } from "lucide-react";
|
||||||
|
import ThemeSwitch from "./ThemeSwitch";
|
||||||
|
import { go } from "../utils/navigation";
|
||||||
|
|
||||||
|
export default function TitleBar({ title, theme, setTheme, compact = false }) {
|
||||||
|
return (
|
||||||
|
<header className="glass-bar relative z-10 flex items-center justify-between gap-4 border-b border-line px-5 py-2">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="truncate text-[18px] font-extrabold leading-tight">{title}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center gap-5">
|
||||||
|
<ThemeSwitch theme={theme} setTheme={setTheme} />
|
||||||
|
{!compact ? (
|
||||||
|
<>
|
||||||
|
<button className="icon-btn" aria-label="重启"><RotateCcw size={16} strokeWidth={2.5} /></button>
|
||||||
|
<button className="icon-btn" aria-label="退出"><LogOut size={16} strokeWidth={2.5} /></button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="btn-secondary h-9 px-3" onClick={() => go("/")}>关闭</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/components/ToggleRow.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function ToggleRow({ label, enabled }) {
|
||||||
|
const [checked, setChecked] = useState(enabled);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-[14px] border border-line bg-surface p-4">
|
||||||
|
<span className="min-w-0 text-[14px] font-bold">{label}</span>
|
||||||
|
<button
|
||||||
|
className={`mini-switch ${checked ? "is-on" : ""}`}
|
||||||
|
aria-label={label}
|
||||||
|
aria-pressed={checked}
|
||||||
|
onClick={() => setChecked((value) => !value)}
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/data/mockData.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { BookOpen, Briefcase, FileText, Image, MessageCircle, Users, Video, Wrench } from "lucide-react";
|
||||||
|
|
||||||
|
export const tabs = [
|
||||||
|
{ id: "clone", label: "微信分身", icon: MessageCircle, badge: "3" },
|
||||||
|
{ id: "knowledge", label: "知识库", icon: BookOpen, badge: "" },
|
||||||
|
{ id: "skills", label: "skill市场", icon: Wrench, badge: "新" },
|
||||||
|
{ id: "distill", label: "员工蒸馏", icon: Users, badge: "" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const knowledgeTypeIcons = {
|
||||||
|
视频: Video,
|
||||||
|
图片: Image,
|
||||||
|
文件: FileText,
|
||||||
|
案例: Briefcase,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagToneClasses = ["tag-tone-green", "tag-tone-orange", "tag-tone-purple"];
|
||||||
|
|
||||||
|
export const logs = [
|
||||||
|
{ level: "info", message: "[20:16:11] engine status: waiting for local wechat session" },
|
||||||
|
{ level: "info", message: "[20:16:14] pi-agent rpc heartbeat ready" },
|
||||||
|
{ level: "info", message: "[20:16:16] knowledge retriever loaded: FTS5/BM25" },
|
||||||
|
{ level: "warning", message: "[20:16:18] wechat adapter latency is higher than expected: 184ms" },
|
||||||
|
{ level: "info", message: "[20:16:21] model gateway connected: doubao-compatible" },
|
||||||
|
{ level: "info", message: "[20:16:28] reply policy: whitelist + skill authorization enabled" },
|
||||||
|
{ level: "warning", message: "[20:16:34] employee skill '售后安抚助手' is disabled, fallback to base prompt" },
|
||||||
|
{ level: "info", message: "[20:16:41] incoming message normalized: customer_id=wx_9271" },
|
||||||
|
{ level: "info", message: "[20:16:42] retrieval hit: 售后常见问题与退款边界, score=0.86" },
|
||||||
|
{ level: "info", message: "[20:16:44] draft reply generated, tokens=312" },
|
||||||
|
{ level: "warning", message: "[20:16:47] manual confirmation required by托管配置" },
|
||||||
|
{ level: "info", message: "[20:16:53] operator approved message delivery" },
|
||||||
|
{ level: "info", message: "[20:16:54] message delivered to local wechat session" },
|
||||||
|
{ level: "error", message: "[20:17:02] media parser failed: unsupported image format webp-alpha" },
|
||||||
|
{ level: "warning", message: "[20:17:05] retry media parser with safe mode" },
|
||||||
|
{ level: "info", message: "[20:17:07] media parser recovered: generated OCR summary" },
|
||||||
|
{ level: "info", message: "[20:17:12] sqlite config synced: 7 keys updated" },
|
||||||
|
{ level: "error", message: "[20:17:19] model gateway timeout after 8000ms, request_id=req_91fa" },
|
||||||
|
{ level: "warning", message: "[20:17:20] switched to backup model endpoint" },
|
||||||
|
{ level: "info", message: "[20:17:23] backup model endpoint connected" },
|
||||||
|
{ level: "info", message: "[20:17:30] background distill queue idle" },
|
||||||
|
{ level: "info", message: "[20:17:36] heartbeat ok: memory=128MB, queue=0" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const knowledgeItems = [
|
||||||
|
{ title: "销售冠军高意向客户跟进案例", type: "案例", updated: "2026-06-08 18:20", refs: 128, version: "v2.4", status: "已生效" },
|
||||||
|
{ title: "售后常见问题与退款边界", type: "文件", updated: "2026-06-05 09:42", refs: 82, version: "v1.7", status: "已生效" },
|
||||||
|
{ title: "企业微信客户分层话术图片包", type: "图片", updated: "2026-05-30 14:12", refs: 41, version: "v1.1", status: "未生效" },
|
||||||
|
{ title: "直播间私域导流短视频脚本", type: "视频", updated: "2026-05-24 22:05", refs: 35, version: "v0.9", status: "已停用" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const skillItems = [
|
||||||
|
{ name: "客户意图识别", type: "内置", enabled: true, tags: ["线索分级", "异议判断", "下一步建议"] },
|
||||||
|
{ name: "报价策略生成", type: "自定义", enabled: true, tags: ["利润保护", "阶梯报价", "优惠边界"] },
|
||||||
|
{ name: "售后安抚助手", type: "内置", enabled: false, tags: ["情绪识别", "补偿建议", "工单摘要"] },
|
||||||
|
{ name: "朋友圈内容策划", type: "自定义", enabled: true, tags: ["素材改写", "发布时间", "人设统一"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const employeeItems = [
|
||||||
|
{ name: "销售冠军 Aileen", version: "v3.2", enabled: true, traits: "高意向逼单 / 异议拆解 / 温和推进", domain: "企微私域成交" },
|
||||||
|
{ name: "售前专家 Leo", version: "v1.8", enabled: true, traits: "需求澄清 / 方案对齐 / 技术问答", domain: "B2B咨询" },
|
||||||
|
{ name: "客服主管 Mina", version: "v2.1", enabled: false, traits: "投诉降温 / 售后边界 / 情绪安抚", domain: "售后服务" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const settingSections = [
|
||||||
|
"基础配置",
|
||||||
|
"提示词配置",
|
||||||
|
"回复规则",
|
||||||
|
"skill管理",
|
||||||
|
"员工管理",
|
||||||
|
"知识库管理",
|
||||||
|
"托管配置",
|
||||||
|
];
|
||||||
368
src/index.css
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f7f8f7;
|
||||||
|
--page: #ffffff;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #f2f5f3;
|
||||||
|
--text: #151716;
|
||||||
|
--muted: #66716c;
|
||||||
|
--subtle: #909a95;
|
||||||
|
--line: #dde4df;
|
||||||
|
--line-strong: #cad5cf;
|
||||||
|
--primary: #07c160;
|
||||||
|
--primary-2: #10d978;
|
||||||
|
--primary-soft: #e6f7ee;
|
||||||
|
--primary-soft-2: #f1fbf6;
|
||||||
|
--accent: #2d8cff;
|
||||||
|
--warning: #fa8c16;
|
||||||
|
--error: #f5222d;
|
||||||
|
--shadow: 0 24px 70px rgba(20, 36, 27, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #090d0b;
|
||||||
|
--page: #0d1110;
|
||||||
|
--surface: #121817;
|
||||||
|
--surface-2: #18211e;
|
||||||
|
--text: #f4f7f5;
|
||||||
|
--muted: #aab5af;
|
||||||
|
--subtle: #7f8c85;
|
||||||
|
--line: #24302b;
|
||||||
|
--line-strong: #34423c;
|
||||||
|
--primary: #07c160;
|
||||||
|
--primary-2: #0ed46e;
|
||||||
|
--primary-soft: #0d2a1b;
|
||||||
|
--primary-soft-2: #10251a;
|
||||||
|
--accent: #2d8cff;
|
||||||
|
--warning: #fa8c16;
|
||||||
|
--error: #ff4d4f;
|
||||||
|
--shadow: 0 28px 80px rgba(0, 0, 0, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 10%, rgba(7, 193, 96, 0.11), transparent 30%),
|
||||||
|
radial-gradient(circle at 86% 4%, rgba(45, 140, 255, 0.1), transparent 24%),
|
||||||
|
var(--bg);
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.app-window {
|
||||||
|
position: relative;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.54),
|
||||||
|
inset 0 -1px 0 rgba(128, 128, 128, 0.08),
|
||||||
|
var(--shadow);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 16% 4%, rgba(7, 193, 96, 0.12), transparent 28%),
|
||||||
|
radial-gradient(circle at 86% 2%, rgba(45, 140, 255, 0.11), transparent 24%),
|
||||||
|
radial-gradient(circle at 50% 36%, rgba(7, 193, 96, 0.08), transparent 30%),
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--page) 68%, transparent), color-mix(in srgb, var(--page) 82%, transparent) 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .app-window {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||||
|
inset 0 -1px 0 rgba(255, 255, 255, 0.04),
|
||||||
|
var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-bar {
|
||||||
|
background:
|
||||||
|
linear-gradient(10deg, color-mix(in srgb, var(--surface) 44%, transparent), color-mix(in srgb, var(--surface) 22%, transparent));
|
||||||
|
backdrop-filter: blur(10px) saturate(1.35);
|
||||||
|
-webkit-backdrop-filter: blur(28px) saturate(1.35);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.36),
|
||||||
|
0 10px 28px rgba(20, 36, 27, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
@apply rounded-[18px] border border-line bg-surface shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-panel {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 30%, rgba(7, 193, 96, 0.16), transparent 26%),
|
||||||
|
radial-gradient(circle at 35% 12%, rgba(45, 140, 255, 0.08), transparent 28%),
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--surface) 88%, transparent), var(--surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-panel > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 34%, rgba(7, 193, 96, 0.18), transparent 22%),
|
||||||
|
radial-gradient(circle at 50% 46%, rgba(7, 193, 96, 0.1), transparent 34%);
|
||||||
|
filter: blur(18px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply inline-flex h-10 items-center justify-center rounded-full border-0 bg-primary px-5 text-[14px] font-extrabold text-white shadow-[0_10px_28px_rgba(7,193,96,.22)] transition hover:bg-[#05964d];
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply inline-flex h-10 items-center justify-center rounded-full border-0 bg-error px-5 text-[14px] font-extrabold text-white shadow-[0_10px_28px_rgba(245,34,45,.22)] transition hover:bg-[#cf1322];
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply inline-flex h-10 items-center justify-center rounded-full border border-line bg-surface px-4 text-[14px] font-bold text-text transition hover:bg-surface2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test {
|
||||||
|
@apply inline-flex h-10 items-center justify-center rounded-full border border-[#b8dec9] bg-[#f2faf5] px-4 text-[14px] font-bold text-[#3d7a58] transition hover:bg-[#e8f5ee];
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .btn-test {
|
||||||
|
@apply border-[#294236] bg-[#101b16] text-[#8fbea3] hover:bg-[#14251d];
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
@apply inline-flex h-9 items-center justify-center rounded-[10px] border-0 bg-transparent px-3 text-[13px] font-bold text-muted transition hover:bg-surface2 hover:text-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
@apply grid h-9 w-9 place-items-center rounded-[10px] border-0 bg-transparent text-[12px] font-black text-muted transition hover:bg-transparent hover:text-text active:bg-transparent focus:bg-transparent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-action {
|
||||||
|
@apply grid h-10 w-10 place-items-center rounded-full border border-line bg-surface text-[13px] font-black text-muted shadow-[0_10px_30px_rgba(0,0,0,.04)] transition hover:bg-surface2 hover:text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box,
|
||||||
|
.icon-box-sm {
|
||||||
|
@apply grid shrink-0 place-items-center rounded-[12px] border border-line bg-surface2 text-[12px] font-black text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
@apply h-10 w-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box-sm {
|
||||||
|
@apply h-7 w-7 rounded-[9px] text-[11px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box-strong {
|
||||||
|
@apply border-0 bg-primary text-white shadow-[0_10px_28px_rgba(7,193,96,.24)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-active {
|
||||||
|
@apply border-primary bg-[var(--primary-soft)] text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
@apply absolute right-4 top-1 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-black leading-none text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
@apply inline-flex h-6 items-center rounded-full px-2.5 text-[11px] font-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .status-warning {
|
||||||
|
color: #ffad4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-muted {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 0%, rgba(7, 193, 96, 0.08), transparent 30%),
|
||||||
|
linear-gradient(145deg, #0b100d, #101814 52%, #090f0c);
|
||||||
|
@apply overflow-hidden rounded-[10px] border border-line p-4 shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line-info {
|
||||||
|
color: #8ff0b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line-warning {
|
||||||
|
color: #ffd166;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line-error {
|
||||||
|
color: #ff6b7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
@apply flex items-start justify-between gap-3 rounded-[8px] border border-line bg-surface p-3 shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
@apply rounded-full border border-line bg-surface2 px-3 py-1 text-[10px] font-bold text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-tone-green {
|
||||||
|
@apply border-[#bfe8d1] bg-[#edf9f2] text-[#2f8a57];
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-tone-orange {
|
||||||
|
@apply border-[#f3d6a6] bg-[#fff7e8] text-[#9a651b];
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-tone-purple {
|
||||||
|
@apply border-[#dfd1f2] bg-[#f7f0ff] text-[#7650a8];
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-type{
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-state-enabled {
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-state-disabled {
|
||||||
|
@apply text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .tag-tone-green {
|
||||||
|
@apply border-[#244735] bg-[#102119] text-[#8bd6a8];
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .tag-tone-orange {
|
||||||
|
@apply border-[#4b3720] bg-[#241a0e] text-[#e5ba70];
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .tag-tone-purple {
|
||||||
|
@apply border-[#3e3153] bg-[#1f182b] text-[#c4a2ef];
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .skill-type[data-skill-type="内置"] {
|
||||||
|
color: #d2a75d;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .skill-type[data-skill-type="自定义"] {
|
||||||
|
color: #c4a2ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
@apply fixed bottom-24 right-[max(24px,calc((100vw-750px)/2+24px))] grid h-14 w-14 place-items-center rounded-full bg-primary text-[30px] font-light leading-none text-white shadow-[0_18px_36px_rgba(7,193,96,.28)] transition hover:bg-[#05964d];
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-like {
|
||||||
|
@apply flex h-[42px] w-full items-center rounded-[11px] border border-lineStrong bg-surface px-3 text-[13px] font-semibold text-text outline-none transition placeholder:text-subtle focus:border-primary disabled:cursor-not-allowed disabled:bg-surface2 disabled:text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
@apply min-h-[160px] w-full resize-none overflow-y-auto rounded-[8px] border border-lineStrong bg-surface p-2 font-mono text-[13px] leading-6 text-text outline-none transition focus:border-primary disabled:text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-switch {
|
||||||
|
@apply relative h-7 w-12 shrink-0 rounded-full bg-lineStrong p-[3px] transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-switch span {
|
||||||
|
@apply block h-[22px] w-[22px] rounded-full bg-white transition-transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-switch.is-on {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-switch.is-on span {
|
||||||
|
@apply translate-x-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-sphere {
|
||||||
|
position: relative;
|
||||||
|
width: 190px;
|
||||||
|
height: 190px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 32% 24%, rgba(255, 255, 255, 0.95), transparent 12%),
|
||||||
|
radial-gradient(circle at 42% 36%, rgba(255, 255, 255, 0.38), transparent 20%),
|
||||||
|
radial-gradient(circle at 62% 72%, rgba(0, 0, 0, 0.22), transparent 34%),
|
||||||
|
linear-gradient(145deg, #10d978, #07c160 48%, #047a40);
|
||||||
|
box-shadow:
|
||||||
|
inset -22px -28px 54px rgba(0, 0, 0, 0.24),
|
||||||
|
inset 18px 18px 40px rgba(255, 255, 255, 0.18),
|
||||||
|
0 34px 80px rgba(7, 193, 96, 0.28);
|
||||||
|
animation: floatSphere 4.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-sphere::before,
|
||||||
|
.engine-sphere::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||||
|
transform: rotate(-24deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-sphere::after {
|
||||||
|
inset: 42px 18px;
|
||||||
|
opacity: 0.68;
|
||||||
|
transform: rotate(22deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatSphere {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate3d(0, -12px, 0) rotate(8deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.engine-sphere {
|
||||||
|
width: 154px;
|
||||||
|
height: 154px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
right: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
36
src/pages/ClonePage.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Power, PowerOff, Settings } from "lucide-react";
|
||||||
|
import { logs } from "../data/mockData";
|
||||||
|
import StatusPill from "../components/StatusPill";
|
||||||
|
import Terminal from "../components/Terminal";
|
||||||
|
import { go } from "../utils/navigation";
|
||||||
|
|
||||||
|
export default function ClonePage() {
|
||||||
|
const [engineEnabled, setEngineEnabled] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex min-h-full flex-1 flex-col gap-3">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<StatusPill tone="success" label="大模型已连接" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-5 px-6 py-2">
|
||||||
|
<div className="engine-sphere engine-success" aria-hidden="true" />
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-[22px] font-extrabold">微信引擎运行中</h2>
|
||||||
|
<p className="mt-2 text-[13px] text-muted">微信适配层已就绪,等待本地消息事件。</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-center gap-4">
|
||||||
|
<button
|
||||||
|
className={`${engineEnabled ? "btn-danger" : "btn-primary"} w-full gap-2`}
|
||||||
|
onClick={() => setEngineEnabled((enabled) => !enabled)}
|
||||||
|
>
|
||||||
|
{engineEnabled ? <PowerOff size={16} strokeWidth={2.6} /> : <Power size={16} strokeWidth={2.6} />}
|
||||||
|
{engineEnabled ? "停用引擎" : "启动引擎"}
|
||||||
|
</button>
|
||||||
|
<button className="icon-action shrink-0" onClick={() => go("/window/settings")} aria-label="设置"><Settings size={18} strokeWidth={2.5} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Terminal title="运行日志" lines={logs} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/pages/DistillPage.jsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { employeeItems } from "../data/mockData";
|
||||||
|
import { go } from "../utils/navigation";
|
||||||
|
|
||||||
|
export default function DistillPage() {
|
||||||
|
return (
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div className=" p-5 text-center">
|
||||||
|
<h2 className="text-[24px] font-extrabold leading-tight">把销售冠军蒸馏成员工 skill</h2>
|
||||||
|
<p className="mt-3 text-[13px] text-muted">导入聊天记录、话术和案例,沉淀可授权调用的员工能力。</p>
|
||||||
|
<div className="mt-5 w-full flex flex-wrap gap-8">
|
||||||
|
<button className="btn-secondary flex-1">蒸馏教程</button>
|
||||||
|
<button className="btn-primary flex-1" onClick={() => go("/window/distill-start")}>开始蒸馏</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel rounded-md p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-[15px] font-extrabold">销售冠军 Aileen 话术蒸馏</h3>
|
||||||
|
<p className="mt-1 text-[12px] text-muted">
|
||||||
|
<span className="text-primary">成功 248 条</span>
|
||||||
|
<span className="mx-1 text-subtle">·</span>
|
||||||
|
<span className="text-error">失败 7 条</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[13px] font-black text-primary">72%</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 h-2 rounded-full bg-surface2">
|
||||||
|
<div className="h-full w-[72%] rounded-full bg-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{employeeItems.map((item) => (
|
||||||
|
<article key={item.name} className="panel rounded-md p-3" onClick={() => go("/window/employee-detail")}>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<h3 className="min-w-0 truncate text-[15px] font-extrabold">{item.name}</h3>
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5 text-[12px] font-semibold text-muted">
|
||||||
|
<span>{item.version}</span>
|
||||||
|
<span className="text-subtle">·</span>
|
||||||
|
<span className={item.enabled ? "skill-state-enabled" : "skill-state-disabled"}>{item.enabled ? "已启用" : "已关闭"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-[13px] leading-6 text-muted">{item.traits}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/pages/KnowledgePage.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { knowledgeItems } from "../data/mockData";
|
||||||
|
import DraggableFab from "../components/DraggableFab";
|
||||||
|
import Segmented from "../components/Segmented";
|
||||||
|
import StatusPill from "../components/StatusPill";
|
||||||
|
import { go } from "../utils/navigation";
|
||||||
|
|
||||||
|
export default function KnowledgePage({ active, setActive }) {
|
||||||
|
const types = ["全部", "视频", "图片", "文件", "案例", "其他"];
|
||||||
|
const filtered = active === "全部" ? knowledgeItems : knowledgeItems.filter((item) => item.type === active);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-3 pb-20">
|
||||||
|
<Segmented items={types} active={active} onChange={setActive} />
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filtered.map((item) => (
|
||||||
|
<article key={item.title} className="list-card" onClick={() => go("/window/knowledge-detail")}>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="truncate text-[14px] font-extrabold">{item.title}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-[12px] text-muted">更新 {item.updated} · 引用 {item.refs} 次</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 self-center items-center justify-center gap-4">
|
||||||
|
<span className="text-[12px] font-bold text-warning">{item.version}</span>
|
||||||
|
<StatusPill tone={item.status === "已生效" ? "success" : item.status === "未生效" ? "warning" : "muted"} label={item.status} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DraggableFab onClick={() => go("/window/knowledge-create")} ariaLabel="新增知识库" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/pages/SkillsPage.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { skillItems, tagToneClasses } from "../data/mockData";
|
||||||
|
import DraggableFab from "../components/DraggableFab";
|
||||||
|
import Segmented from "../components/Segmented";
|
||||||
|
import { go } from "../utils/navigation";
|
||||||
|
|
||||||
|
export default function SkillsPage({ active, setActive }) {
|
||||||
|
const types = ["全部", "内置", "自定义"];
|
||||||
|
const filtered = active === "全部" ? skillItems : skillItems.filter((item) => item.type === active);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-4 pb-20">
|
||||||
|
<Segmented items={types} active={active} onChange={setActive} />
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filtered.map((skill) => (
|
||||||
|
<article key={skill.name} className="panel rounded-md p-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
<div className="flex w-full min-w-0 items-center justify-between gap-4">
|
||||||
|
<h3 className="truncate text-[15px] font-extrabold">{skill.name}</h3>
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5 text-[12px] font-semibold">
|
||||||
|
<span className="skill-type">{skill.type}</span>
|
||||||
|
<span className="text-subtle">·</span>
|
||||||
|
<span className={skill.enabled ? "skill-state-enabled" : "skill-state-disabled"}>{skill.enabled ? "已启用" : "已关闭"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{skill.tags.map((tag, index) => <span key={tag} className={`tag ${tagToneClasses[index % tagToneClasses.length]}`}>{tag}</span>)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DraggableFab onClick={() => go("/window/skill-create")} ariaLabel="新增 skill" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/utils/navigation.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function go(path) {
|
||||||
|
window.location.hash = path === "/" ? "" : path;
|
||||||
|
}
|
||||||
18
src/windows/PlaceholderWindow.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Bot } from "lucide-react";
|
||||||
|
import IconBox from "../components/IconBox";
|
||||||
|
|
||||||
|
export default function PlaceholderWindow({ title, route }) {
|
||||||
|
return (
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="panel grid min-h-[520px] place-items-center text-center">
|
||||||
|
<div className="max-w-[360px]">
|
||||||
|
<IconBox icon={Bot} strong />
|
||||||
|
<h2 className="mt-5 text-[24px] font-extrabold">{title}</h2>
|
||||||
|
<p className="mt-3 text-[13px] leading-6 text-muted">
|
||||||
|
当前二级窗口先按需求保留占位内容。路由为 <span className="font-mono text-primary">{route}</span>,后续可接入独立业务表单或详情渲染。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
src/windows/SecondaryWindow.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { logs } from "../data/mockData";
|
||||||
|
import Terminal from "../components/Terminal";
|
||||||
|
import TitleBar from "../components/TitleBar";
|
||||||
|
import PlaceholderWindow from "./PlaceholderWindow";
|
||||||
|
import SettingsWindow from "./SettingsWindow";
|
||||||
|
|
||||||
|
export default function SecondaryWindow({ route, theme, setTheme, settingSection, setSettingSection }) {
|
||||||
|
const title = useMemo(() => {
|
||||||
|
if (route.includes("settings")) return "设置";
|
||||||
|
if (route.includes("engine-logs")) return "分身引擎日志";
|
||||||
|
if (route.includes("knowledge-create")) return "新增知识库";
|
||||||
|
if (route.includes("knowledge-detail")) return "知识库详情";
|
||||||
|
if (route.includes("skill")) return "skill技能详情";
|
||||||
|
if (route.includes("employee-detail")) return "员工蒸馏详情";
|
||||||
|
if (route.includes("distill-start")) return "开始蒸馏";
|
||||||
|
return "二级窗口";
|
||||||
|
}, [route]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-window mx-auto flex min-h-[calc(100dvh-32px)] w-full max-w-[750px] flex-col overflow-hidden rounded-[10px] border border-line shadow-panel">
|
||||||
|
<TitleBar title={title} subtitle="750 x 950 Web 窗口预览" theme={theme} setTheme={setTheme} compact />
|
||||||
|
<main className="content-canvas flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||||
|
{route.includes("settings") ? (
|
||||||
|
<SettingsWindow active={settingSection} setActive={setSettingSection} />
|
||||||
|
) : route.includes("engine-logs") ? (
|
||||||
|
<div className="flex h-full min-h-0 p-5"><Terminal title="完整运行日志" lines={[...logs, ...logs, ...logs]} tall /></div>
|
||||||
|
) : (
|
||||||
|
<PlaceholderWindow title={title} route={route} />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/windows/SettingsContent.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { employeeItems, knowledgeItems, skillItems } from "../data/mockData";
|
||||||
|
import EditorBlock from "../components/EditorBlock";
|
||||||
|
import Field from "../components/Field";
|
||||||
|
import SectionHeading from "../components/SectionHeading";
|
||||||
|
import Terminal from "../components/Terminal";
|
||||||
|
import ToggleRow from "../components/ToggleRow";
|
||||||
|
|
||||||
|
export default function SettingsContent({ active }) {
|
||||||
|
if (active === "基础配置") {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-5">
|
||||||
|
<SectionHeading title="大模型 key 配置" desc="兼容 OpenAI 协议" />
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Field label="模型厂家" value="豆包 / 火山引擎 / DeepSeek / 自定义" disabled />
|
||||||
|
<Field label="请求地址" value="https://api.example.com/v1" />
|
||||||
|
<Field label="API Key" value="sk-****************" secret />
|
||||||
|
<Field label="模型名称" value="doubao-pro-32k" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button className="btn-primary flex-1">保存</button>
|
||||||
|
<button className="btn-test flex-1">测试连接</button>
|
||||||
|
</div>
|
||||||
|
<Terminal
|
||||||
|
title="大模型请求记录"
|
||||||
|
lines={[
|
||||||
|
{ level: "info", message: "[ready] 等待测试连接" },
|
||||||
|
{ level: "warning", message: "[hint] 保存后由 Rust command 写入本地配置" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active === "提示词配置") {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-5">
|
||||||
|
<SectionHeading title="提示词配置" desc="固定提示词不可编辑,自定义提示词可保存。" />
|
||||||
|
<EditorBlock title="固定提示词" text="你是本地微信分身助手,需要遵守托管策略、回复边界与用户授权。" />
|
||||||
|
<EditorBlock title="自定义提示词" text="保持友好、简洁、可信赖。优先调用知识库和已授权员工能力。" fill />
|
||||||
|
<button className="btn-primary">保存提示词</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listMap = {
|
||||||
|
回复规则: ["免打扰模式", "自动加好友", "主动联系客户", "开启蒸馏", "拉黑用户名称列表", "白名单用户名称列表"],
|
||||||
|
skill管理: ["开启 skill 功能", ...skillItems.map((item) => item.name)],
|
||||||
|
员工管理: ["开启使用员工功能", ...employeeItems.map((item) => item.name)],
|
||||||
|
知识库管理: ["开启使用知识库功能", ...knowledgeItems.map((item) => item.title)],
|
||||||
|
托管配置: ["开启托管功能", "本地消息服务", "发送前人工确认", "异常自动暂停"],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SectionHeading title={active} desc="授权开关将在接入 Tauri command 后持久化到 sqlite 配置表。" />
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(listMap[active] || []).map((item, index) => (
|
||||||
|
<ToggleRow key={item} label={item} enabled={index % 3 !== 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="btn-primary">保存配置</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/windows/SettingsWindow.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { settingSections } from "../data/mockData";
|
||||||
|
import SettingsContent from "./SettingsContent";
|
||||||
|
|
||||||
|
export default function SettingsWindow({ active, setActive }) {
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-0 flex-1 grid-cols-[180px_minmax(0,1fr)] max-sm:grid-cols-1">
|
||||||
|
<aside className="h-full border-r border-line bg-surface2 p-3 max-sm:h-auto max-sm:border-b max-sm:border-r-0">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{settingSections.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
className={`rounded-xl px-3 py-3 text-left text-[13px] font-bold transition ${
|
||||||
|
active === item ? "bg-[var(--primary-soft)] text-primary" : "text-muted hover:bg-surface"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActive(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<section className="flex h-full min-h-0 flex-1 flex-col gap-5 p-5">
|
||||||
|
<SettingsContent active={active} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
tailwind.config.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export default {
|
||||||
|
darkMode: ["selector", '[data-theme="dark"]'],
|
||||||
|
content: ["./index.html", "./src/**/*.{js,jsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: [
|
||||||
|
"Inter",
|
||||||
|
"ui-sans-serif",
|
||||||
|
"system-ui",
|
||||||
|
"-apple-system",
|
||||||
|
"BlinkMacSystemFont",
|
||||||
|
"Segoe UI",
|
||||||
|
"PingFang SC",
|
||||||
|
"Microsoft YaHei",
|
||||||
|
"sans-serif",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
bg: "var(--bg)",
|
||||||
|
page: "var(--page)",
|
||||||
|
surface: "var(--surface)",
|
||||||
|
surface2: "var(--surface-2)",
|
||||||
|
text: "var(--text)",
|
||||||
|
muted: "var(--muted)",
|
||||||
|
subtle: "var(--subtle)",
|
||||||
|
line: "var(--line)",
|
||||||
|
lineStrong: "var(--line-strong)",
|
||||||
|
primary: "var(--primary)",
|
||||||
|
accent: "var(--accent)",
|
||||||
|
warning: "var(--warning)",
|
||||||
|
error: "var(--error)",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
panel: "var(--shadow)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
30
vite.config.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
clearScreen: false,
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || '127.0.0.1',
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: 'ws',
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
ignored: ['**/src-tauri/**'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
envPrefix: ['VITE_', 'TAURI_ENV_*'],
|
||||||
|
build: {
|
||||||
|
target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
|
||||||
|
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
|
||||||
|
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||||
|
},
|
||||||
|
});
|
||||||