commit 045fd9dc0132a0b79b5dfb34144fe956a379e80d Author: hy Date: Mon Dec 22 15:55:15 2025 +0800 first commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cf640d53 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..75ef73cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +out +.DS_Store +.eslintcache +*.log* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9c6b791d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +out +dist +pnpm-lock.yaml +LICENSE.md +tsconfig.json +tsconfig.*.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 00000000..35893b3b --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,4 @@ +singleQuote: true +semi: false +printWidth: 100 +trailingComma: none diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 00000000..43760d26 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,67 @@ +# NCurator-Desktop 项目规则与指南 + +## 1. 项目概述 +NCurator-Desktop 是一个基于 Electron 构建的本地知识库搜索应用程序。它专注于隐私和离线能力,通过本地处理文档。 +- **目标文件**: PDF (`.pdf`), Word (`.docx`)。 +- **核心功能**: 摄入、索引和混合搜索(向量 + 关键词)。 + +## 2. 技术栈 + +### 核心 & 桌面 +- **运行时**: Electron (主进程负责逻辑,渲染进程负责 UI)。 +- **语言**: TypeScript。 + +### 前端 (渲染器) +- **框架**: React。 +- **UI 库**: Ant Design (antd)。 +- **样式**: Tailwind CSS。 +- **状态管理**: React Hooks / Context (或用户首选)。 + +### 后端 / 逻辑 (Node.js / 主进程) +- **文档解析**: `langchain` (JS 版本) 用于解析 PDF 和 DOCX。 +- **文本分片**: `langchain` 分片器。 +- **向量化**: `@xenova/transformers` (Transformers.js) 用于本地嵌入生成。 +- **向量数据库**: `lancedb` 用于本地向量存储。 +- **关键词搜索**: `flexsearch` 用于倒排索引和关键词匹配。 +- **重排序**: 自定义实现的 RRF (Reciprocal Rank Fusion) 算法。 + +## 3. 架构与实现流程 + +### 3.1 数据摄入管道 +1. **解析**: 使用 LangChain JS 加载器从 `.pdf` 和 `.docx` 文件中提取文本。 +2. **分片**: 使用 LangChain 文本分片器将文本分割成可管理的块。 +3. **向量化**: + - 使用 Transformers.js 为文本块生成嵌入。 + - **模型**: + - 中文内容: `jinaai/jina-embeddings-v2-base-zh` + - 英文内容: `nomic-ai/nomic-embed-text-v1` +4. **存储**: + - **向量存储**: 将向量和元数据保存到 LanceDB。 + - **倒排索引**: 将文本块索引到 FlexSearch 以进行关键词检索。 + +### 3.2 搜索管道 +1. **召回 (混合)**: + - **关键词搜索**: 查询 FlexSearch -> 获取 Top 50 结果。 + - **向量搜索**: 查询 LanceDB (Bi-Encoder) -> 获取 Top 50 结果。 +2. **粗排 (融合)**: + - 应用 **RRF (Reciprocal Rank Fusion)** 算法对来自两个来源的结果进行合并和排序。 +3. **重排 (未来/优化)**: + - *注: 目前推迟,但架构应允许实现。* + - 从 RRF 结果中选择 Top 10-20。 + - 使用 Cross-Encoder (`Xenova/bge-reranker-base`) 进行精细打分。 + +## 4. 用户配置 +- 用户必须能够在设置中选择模型。 +- 默认为上述指定的中文/英文推荐模型。 + +## 5. 编码规范 +- **语言**: 始终使用 TypeScript。 +- **异步/等待**: 对文件 I/O 和模型推理使用现代异步模式。 +- **IPC**: 使用 `ipcMain` and `ipcRenderer` (通过 `preload`) 在 UI 和主进程中的繁重处理逻辑之间进行通信。 +- **模块化**: 将解析、索引和搜索逻辑与 UI 组件分离。 +- **注释**: 注释复杂的逻辑,特别是 RRF 算法和模型加载步骤。 + +## 6. AI 交互规则 +- 生成代码时,优先考虑技术栈中列出的库。 +- 除非明确要求,否则不要建议基于云的 API (OpenAI 等);专注于本地执行。 +- 确保所有繁重的计算(解析、嵌入)都卸载到主进程或 Worker,而不是渲染线程,以保持 UI 响应。 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..940260d8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0b6b9a64 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" + }, + "runtimeArgs": ["--sourcemap"], + "env": { + "REMOTE_DEBUGGING_PORT": "9222" + } + }, + { + "name": "Debug Renderer Process", + "port": 9222, + "request": "attach", + "type": "chrome", + "webRoot": "${workspaceFolder}/src/renderer", + "timeout": 60000, + "presentation": { + "hidden": true + } + } + ], + "compounds": [ + { + "name": "Debug All", + "configurations": ["Debug Main Process", "Debug Renderer Process"], + "presentation": { + "order": 1 + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4c05394e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..f6f43ab3 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# NCurator-Desktop + +**NCurator-Desktop** 是一个本地优先、注重隐私的知识库搜索应用程序。它允许您使用先进的混合搜索技术对 PDF 和 Word 文档进行索引和搜索——所有操作均在本地机器上运行,无需将数据上传到云端。 + +## 主要功能 + +- **本地处理**: 所有文档解析、向量化和存储都在本地进行。您的数据永远不会离开您的设备。 +- **格式支持**: 支持索引 PDF (`.pdf`) 和 Word (`.docx`) 文档。 +- **混合搜索**: 结合了关键词搜索 (FlexSearch) 的精确性和向量搜索 (LanceDB) 的语义理解能力。 +- **高级排序**: 使用 **RRF (Reciprocal Rank Fusion)** 融合来自不同搜索算法的结果,以获得更高的准确性。 +- **模型选择**: 在不同语言下选择不同的嵌入模型以获得最佳性能。 + +## 技术栈 + +本项目使用: + +- **核心**: Electron, Node.js +- **前端**: React, TypeScript, Ant Design, Tailwind CSS +- **AI & 数据**: + - **解析**: LangChain.js + - **嵌入**: Transformers.js (`@xenova/transformers`) + - **向量数据库**: LanceDB + - **关键词搜索**: FlexSearch + - **重排序**: RRF 算法 (未来支持 Cross-Encoders) + +## 架构概览 + +### 数据摄入 +1. **解析**: 使用 LangChain 提取文件文本。 +2. **分片**: 将文本分割成语义块。 +3. **向量化**: 使用本地模型 (`jina-embeddings-v2-base-zh` 或 `nomic-embed-text-v1`) 将文本块转换为向量。 +4. **存储**: 将向量保存到 LanceDB,并使用 FlexSearch 建立倒排索引。 + +### 搜索流程 +1. **召回**: 从向量搜索和关键词搜索中分别检索前 50 个结果。 +2. **融合**: 使用倒帕累托等级融合 (RRF) 合并结果。 +3. **展示**: 向用户展示最相关的文本块。 + *(未来: 使用 `bge-reranker-base` 对前几名结果进行重排)* + +## 快速开始 + +### 先决条件 +- Node.js (推荐 v18+) +- pnpm 或 npm + +### 安装 + +```bash +# 安装依赖 +npm install +# 或者 +pnpm install +``` + +### 开发 + +```bash +# 运行开发模式 +npm run dev +``` + +### 构建 + +```bash +# 构建生产版本 +npm run build +``` + +## 配置 + +应用程序支持为不同语言配置不同的模型: +- **中文**: `jinaai/jina-embeddings-v2-base-zh` +- **英文**: `nomic-ai/nomic-embed-text-v1` +- **重排序**: `Xenova/bge-reranker-base` (计划中) + +## 许可证 + +[MIT](LICENSE) diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 00000000..38c887b2 --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 00000000..28644aa9 Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 00000000..72c391eb Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 00000000..cf9e8b2c Binary files /dev/null and b/build/icon.png differ diff --git a/dev-app-update.yml b/dev-app-update.yml new file mode 100644 index 00000000..ee33fe90 --- /dev/null +++ b/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: ncurator-desktop-updater diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 00000000..cce8689a --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,43 @@ +appId: com.electron.app +productName: ncurator-desktop +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' +asarUnpack: + - resources/** +win: + executableName: ncurator-desktop +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. + notarize: false +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 00000000..cbe07359 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,16 @@ +import { resolve } from 'path' +import { defineConfig } from 'electron-vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + main: {}, + preload: {}, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer/src') + } + }, + plugins: [react()] + } +}) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..aff5d3f9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,32 @@ +import { defineConfig } from 'eslint/config' +import tseslint from '@electron-toolkit/eslint-config-ts' +import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier' +import eslintPluginReact from 'eslint-plugin-react' +import eslintPluginReactHooks from 'eslint-plugin-react-hooks' +import eslintPluginReactRefresh from 'eslint-plugin-react-refresh' + +export default defineConfig( + { ignores: ['**/node_modules', '**/dist', '**/out'] }, + tseslint.configs.recommended, + eslintPluginReact.configs.flat.recommended, + eslintPluginReact.configs.flat['jsx-runtime'], + { + settings: { + react: { + version: 'detect' + } + } + }, + { + files: ['**/*.{ts,tsx}'], + plugins: { + 'react-hooks': eslintPluginReactHooks, + 'react-refresh': eslintPluginReactRefresh + }, + rules: { + ...eslintPluginReactHooks.configs.recommended.rules, + ...eslintPluginReactRefresh.configs.vite.rules + } + }, + eslintConfigPrettier +) diff --git a/package.json b/package.json new file mode 100644 index 00000000..ff22a234 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "ncurator-desktop", + "version": "1.0.0", + "description": "An Electron application with React and TypeScript", + "main": "./out/main/index.js", + "author": "example.com", + "homepage": "https://electron-vite.org", + "scripts": { + "format": "prettier --write .", + "lint": "eslint --cache .", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "npm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "electron-vite build && electron-builder --mac", + "build:linux": "electron-vite build && electron-builder --linux" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "electron-updater": "^6.3.9" + }, + "devDependencies": { + "@electron-toolkit/eslint-config-prettier": "^3.0.0", + "@electron-toolkit/eslint-config-ts": "^3.1.0", + "@electron-toolkit/tsconfig": "^2.0.0", + "@types/node": "^22.19.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "electron": "^39.2.6", + "electron-builder": "^26.0.12", + "electron-vite": "^5.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "prettier": "^3.7.4", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "typescript": "^5.9.3", + "vite": "^7.2.6" + } +} diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 00000000..cf9e8b2c Binary files /dev/null and b/resources/icon.png differ diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 00000000..06e06b4c --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,74 @@ +import { app, shell, BrowserWindow, ipcMain } from 'electron' +import { join } from 'path' +import { electronApp, optimizer, is } from '@electron-toolkit/utils' +import icon from '../../resources/icon.png?asset' + +function createWindow(): void { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 900, + height: 670, + show: false, + autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false + } + }) + + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + // Set app user model id for windows + electronApp.setAppUserModelId('com.electron') + + // Default open or close DevTools by F12 in development + // and ignore CommandOrControl + R in production. + // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) + + // IPC test + ipcMain.on('ping', () => console.log('pong')) + + createWindow() + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts new file mode 100644 index 00000000..a1536695 --- /dev/null +++ b/src/preload/index.d.ts @@ -0,0 +1,8 @@ +import { ElectronAPI } from '@electron-toolkit/preload' + +declare global { + interface Window { + electron: ElectronAPI + api: unknown + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 00000000..2d185247 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,22 @@ +import { contextBridge } from 'electron' +import { electronAPI } from '@electron-toolkit/preload' + +// Custom APIs for renderer +const api = {} + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error(error) + } +} else { + // @ts-ignore (define in dts) + window.electron = electronAPI + // @ts-ignore (define in dts) + window.api = api +} diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 00000000..e198e05e --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,17 @@ + + + + + Electron + + + + + +
+ + + diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx new file mode 100644 index 00000000..73de51c9 --- /dev/null +++ b/src/renderer/src/App.tsx @@ -0,0 +1,35 @@ +import Versions from './components/Versions' +import electronLogo from './assets/electron.svg' + +function App(): React.JSX.Element { + const ipcHandle = (): void => window.electron.ipcRenderer.send('ping') + + return ( + <> + logo +
Powered by electron-vite
+
+ Build an Electron app with React +  and TypeScript +
+

+ Please try pressing F12 to open the devTool +

+
+
+ + Documentation + +
+
+ + Send IPC + +
+
+ + + ) +} + +export default App diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css new file mode 100644 index 00000000..5ed6406a --- /dev/null +++ b/src/renderer/src/assets/base.css @@ -0,0 +1,67 @@ +:root { + --ev-c-white: #ffffff; + --ev-c-white-soft: #f8f8f8; + --ev-c-white-mute: #f2f2f2; + + --ev-c-black: #1b1b1f; + --ev-c-black-soft: #222222; + --ev-c-black-mute: #282828; + + --ev-c-gray-1: #515c67; + --ev-c-gray-2: #414853; + --ev-c-gray-3: #32363f; + + --ev-c-text-1: rgba(255, 255, 245, 0.86); + --ev-c-text-2: rgba(235, 235, 245, 0.6); + --ev-c-text-3: rgba(235, 235, 245, 0.38); + + --ev-button-alt-border: transparent; + --ev-button-alt-text: var(--ev-c-text-1); + --ev-button-alt-bg: var(--ev-c-gray-3); + --ev-button-alt-hover-border: transparent; + --ev-button-alt-hover-text: var(--ev-c-text-1); + --ev-button-alt-hover-bg: var(--ev-c-gray-2); +} + +:root { + --color-background: var(--ev-c-black); + --color-background-soft: var(--ev-c-black-soft); + --color-background-mute: var(--ev-c-black-mute); + + --color-text: var(--ev-c-text-1); +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +ul { + list-style: none; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/renderer/src/assets/electron.svg b/src/renderer/src/assets/electron.svg new file mode 100644 index 00000000..45ef09cf --- /dev/null +++ b/src/renderer/src/assets/electron.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css new file mode 100644 index 00000000..0179fc4c --- /dev/null +++ b/src/renderer/src/assets/main.css @@ -0,0 +1,171 @@ +@import './base.css'; + +body { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background-image: url('./wavy-lines.svg'); + background-size: cover; + user-select: none; +} + +code { + font-weight: 600; + padding: 3px 5px; + border-radius: 2px; + background-color: var(--color-background-mute); + font-family: + ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; + font-size: 85%; +} + +#root { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin-bottom: 80px; +} + +.logo { + margin-bottom: 20px; + -webkit-user-drag: none; + height: 128px; + width: 128px; + will-change: filter; + transition: filter 300ms; +} + +.logo:hover { + filter: drop-shadow(0 0 1.2em #6988e6aa); +} + +.creator { + font-size: 14px; + line-height: 16px; + color: var(--ev-c-text-2); + font-weight: 600; + margin-bottom: 10px; +} + +.text { + font-size: 28px; + color: var(--ev-c-text-1); + font-weight: 700; + line-height: 32px; + text-align: center; + margin: 0 10px; + padding: 16px 0; +} + +.tip { + font-size: 16px; + line-height: 24px; + color: var(--ev-c-text-2); + font-weight: 600; +} + +.react { + background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: 700; +} + +.ts { + background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: 700; +} + +.actions { + display: flex; + padding-top: 32px; + margin: -6px; + flex-wrap: wrap; + justify-content: flex-start; +} + +.action { + flex-shrink: 0; + padding: 6px; +} + +.action a { + cursor: pointer; + text-decoration: none; + display: inline-block; + border: 1px solid transparent; + text-align: center; + font-weight: 600; + white-space: nowrap; + border-radius: 20px; + padding: 0 20px; + line-height: 38px; + font-size: 14px; + border-color: var(--ev-button-alt-border); + color: var(--ev-button-alt-text); + background-color: var(--ev-button-alt-bg); +} + +.action a:hover { + border-color: var(--ev-button-alt-hover-border); + color: var(--ev-button-alt-hover-text); + background-color: var(--ev-button-alt-hover-bg); +} + +.versions { + position: absolute; + bottom: 30px; + margin: 0 auto; + padding: 15px 0; + font-family: 'Menlo', 'Lucida Console', monospace; + display: inline-flex; + overflow: hidden; + align-items: center; + border-radius: 22px; + background-color: #202127; + backdrop-filter: blur(24px); +} + +.versions li { + display: block; + float: left; + border-right: 1px solid var(--ev-c-gray-1); + padding: 0 20px; + font-size: 14px; + line-height: 14px; + opacity: 0.8; + &:last-child { + border: none; + } +} + +@media (max-width: 720px) { + .text { + font-size: 20px; + } +} + +@media (max-width: 620px) { + .versions { + display: none; + } +} + +@media (max-width: 350px) { + .tip, + .actions { + display: none; + } +} diff --git a/src/renderer/src/assets/wavy-lines.svg b/src/renderer/src/assets/wavy-lines.svg new file mode 100644 index 00000000..d08c6119 --- /dev/null +++ b/src/renderer/src/assets/wavy-lines.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/components/Versions.tsx b/src/renderer/src/components/Versions.tsx new file mode 100644 index 00000000..37a9ff08 --- /dev/null +++ b/src/renderer/src/components/Versions.tsx @@ -0,0 +1,15 @@ +import { useState } from 'react' + +function Versions(): React.JSX.Element { + const [versions] = useState(window.electron.process.versions) + + return ( + + ) +} + +export default Versions diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/renderer/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx new file mode 100644 index 00000000..5905ed1e --- /dev/null +++ b/src/renderer/src/main.tsx @@ -0,0 +1,11 @@ +import './assets/main.css' + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..31bac6e9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..db23a68c --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], + "compilerOptions": { + "composite": true, + "types": ["electron-vite/node"] + } +} diff --git a/tsconfig.web.json b/tsconfig.web.json new file mode 100644 index 00000000..9c16b661 --- /dev/null +++ b/tsconfig.web.json @@ -0,0 +1,19 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "include": [ + "src/renderer/src/env.d.ts", + "src/renderer/src/**/*", + "src/renderer/src/**/*.tsx", + "src/preload/*.d.ts" + ], + "compilerOptions": { + "composite": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@renderer/*": [ + "src/renderer/src/*" + ] + } + } +}