diff --git a/.env.example b/.env.example index 9f29afc..bbba4ed 100644 --- a/.env.example +++ b/.env.example @@ -107,3 +107,30 @@ APP_DOMAIN_URL=http://127.0.0.1 # HOOK_POOL_REDIS_BLOCK_TIMEOUT=5 # HOOK_POOL_REDIS_MAX_RETRIES=3 # HOOK_POOL_WORKER_ID=(随机 UUID) + +# ============================================================================= +# Frontend (Vite) — 前端运行环境变量 +# ============================================================================= + +# API 基础 URL(为空时使用 Vite dev 代理 /api -> localhost:8080) +# VITE_API_BASE_URL=http://localhost:8080 + +# 前端 WebSocket 连接地址(开发模式通过 Vite 代理) +VITE_WS_URL=ws://localhost:5080 + +# API URL(前端 API 调用,通过 Vite 代理时可为空) +VITE_API_URL= + +# WebSocket 连接模式: "raw-ws" | "socketio" +VITE_WS_MODE=raw-ws + +# ============================================================================= +# Frontend: Grafana Faro (RUM) — 前端性能监控(可选) +# ============================================================================= + +# VITE_FARO_ENABLED=false +# VITE_FARO_URL=https://faro.example.com/collect +# VITE_FARO_API_KEY= +# VITE_FARO_APP_NAME=GitDataAIWeb +# VITE_FARO_APP_ENV=production +# VITE_FARO_APP_VERSION=0.0.1 diff --git a/Cargo.lock b/Cargo.lock index 1b6a415..07f3a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3678,6 +3678,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "serde_yaml", "sha1 0.11.0", "sha2 0.11.0", "ssh-key", @@ -9036,6 +9037,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "serde_yaml", "session", "sha1 0.11.0", "sha2 0.11.0", diff --git a/Cargo.toml b/Cargo.toml index b3f0574..b4628dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,7 +133,7 @@ fs2 = "0.4.3" image = "0.25.10" tokio = "1.50.0" tokio-util = "0.7.18" -tokio-stream = "0.1.18" +tokio-stream = { version = "0.1.18", features = ["sync"] } url = "2.5.8" tower = "0.5" num_cpus = "1.17.0" diff --git a/openapi.json b/openapi.json index 28ca416..9041eee 100644 --- a/openapi.json +++ b/openapi.json @@ -7359,6 +7359,158 @@ } } }, + "/api/projects/{project_name}/message-favorites": { + "get": { + "tags": [ + "Project" + ], + "operationId": "project_message_favorites", + "parameters": [ + { + "name": "project_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "List current user's project message favorites", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse_ProjectMessageFavoriteResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/projects/{project_name}/messages/{message_id}/favorite": { + "post": { + "tags": [ + "Project" + ], + "operationId": "project_message_favorite_add", + "parameters": [ + { + "name": "project_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Favorite a project message", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse_ProjectMessageFavoriteItem" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + } + }, + "delete": { + "tags": [ + "Project" + ], + "operationId": "project_message_favorite_remove", + "parameters": [ + { + "name": "project_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Remove a project message favorite" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + } + } + }, "/api/projects/{project_name}/repos": { "get": { "tags": [ @@ -24956,7 +25108,9 @@ "required": [ "uid", "username", - "has_unread_notifications" + "has_unread_notifications", + "language", + "timezone" ], "properties": { "uid": { @@ -24982,6 +25136,12 @@ "type": "integer", "format": "int64", "minimum": 0 + }, + "language": { + "type": "string" + }, + "timezone": { + "type": "string" } } } @@ -27442,6 +27602,136 @@ } } }, + "ApiResponse_ProjectMessageFavoriteItem": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "required": [ + "uid", + "project_uid", + "room_id", + "room_name", + "message_id", + "sender_type", + "content", + "content_type", + "send_at", + "favorited_at" + ], + "properties": { + "uid": { + "type": "string", + "format": "uuid" + }, + "project_uid": { + "type": "string", + "format": "uuid" + }, + "room_id": { + "type": "string", + "format": "uuid" + }, + "room_name": { + "type": "string" + }, + "message_id": { + "type": "string", + "format": "uuid" + }, + "sender_id": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "sender_type": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "content": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "send_at": { + "type": "string", + "format": "date-time" + }, + "favorited_at": { + "type": "string", + "format": "date-time" + } + } + } + } + }, + "ApiResponse_ProjectMessageFavoriteResponse": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "required": [ + "page", + "per_page", + "total", + "list" + ], + "properties": { + "page": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "per_page": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectMessageFavoriteItem" + } + } + } + } + } + }, "ApiResponse_ProjectRepoCreateResponse": { "type": "object", "required": [ @@ -34908,7 +35198,9 @@ "required": [ "uid", "username", - "has_unread_notifications" + "has_unread_notifications", + "language", + "timezone" ], "properties": { "uid": { @@ -34934,6 +35226,12 @@ "type": "integer", "format": "int64", "minimum": 0 + }, + "language": { + "type": "string" + }, + "timezone": { + "type": "string" } } }, @@ -39247,6 +39545,125 @@ } } }, + "ProjectMessageFavoriteItem": { + "type": "object", + "required": [ + "uid", + "project_uid", + "room_id", + "room_name", + "message_id", + "sender_type", + "content", + "content_type", + "send_at", + "favorited_at" + ], + "properties": { + "uid": { + "type": "string", + "format": "uuid" + }, + "project_uid": { + "type": "string", + "format": "uuid" + }, + "room_id": { + "type": "string", + "format": "uuid" + }, + "room_name": { + "type": "string" + }, + "message_id": { + "type": "string", + "format": "uuid" + }, + "sender_id": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "sender_type": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "content": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "send_at": { + "type": "string", + "format": "date-time" + }, + "favorited_at": { + "type": "string", + "format": "date-time" + } + } + }, + "ProjectMessageFavoriteQuery": { + "type": "object", + "properties": { + "page": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + }, + "per_page": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + } + } + }, + "ProjectMessageFavoriteResponse": { + "type": "object", + "required": [ + "page", + "per_page", + "total", + "list" + ], + "properties": { + "page": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "per_page": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectMessageFavoriteItem" + } + } + } + }, "ProjectModel": { "type": "object", "required": [ @@ -43614,7 +44031,7 @@ "type": "object", "required": [ "uid", - "project_uid", + "user_uid", "amount", "currency", "reason", @@ -43626,16 +44043,16 @@ "format": "uuid" }, "project_uid": { - "type": "string", - "format": "uuid" - }, - "user_uid": { "type": [ "string", "null" ], "format": "uuid" }, + "user_uid": { + "type": "string", + "format": "uuid" + }, "amount": { "type": "number", "format": "double" @@ -44410,4 +44827,4 @@ "description": "AI conversation and messaging" } ] -} \ No newline at end of file +} diff --git a/src/client/aiChatApi.ts b/src/client/aiChatApi.ts index 6613838..7e23746 100644 --- a/src/client/aiChatApi.ts +++ b/src/client/aiChatApi.ts @@ -157,9 +157,59 @@ export async function shareConversation(conversationId: string): Promise<{ share } export interface StreamChunk { - type: "token" | "thinking" | "tool_call" | "tool_result" | "done" | "error" | "title" | "billing_error"; + type: "token" | "thinking" | "tool_call" | "tool_result" | "done" | "stopped" | "error" | "title" | "billing_error"; // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any; + children_id?: string; +} + +async function* streamConversationWatch(conversationId: string): AsyncGenerator { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/conversations/${conversationId}/watch`, { + method: "GET", + credentials: "include", + }); + + if (!response.ok) throw new Error(`Watch stream request failed: ${response.status}`); + if (!response.body) throw new Error("No response body"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith("data: ")) continue; + + try { + const parsed = JSON.parse(trimmed.slice(6)); + const eventType = parsed.type || parsed.event; + const payload = parsed.data; + + if (eventType === "message" && payload?.role === "assistant" && payload?.content) { + yield { type: "done", data: "ok" }; + return; + } + + const chunkType = payload?.chunk_type || eventType; + if (chunkType === "token" || chunkType === "thinking" || chunkType === "tool_call" || chunkType === "tool_result" || chunkType === "error") { + yield { + type: chunkType, + data: payload?.content ?? payload?.error ?? payload, + }; + } + } catch { + // Ignore comments and malformed events. + } + } + } } export async function* streamChat(conversationId: string, messageId: string): AsyncGenerator { @@ -197,8 +247,12 @@ export async function* streamChat(conversationId: string, messageId: string): As const parsed = JSON.parse(jsonStr); // Normalize backend SSE format: {event: "token", data: "..."} → {type: "token", data: "..."} const eventType = parsed.type || parsed.event; + if (eventType === "recovery") { + yield* streamConversationWatch(conversationId); + return; + } if (eventType === "token" || eventType === "thinking" || eventType === "tool_call" || eventType === "tool_result" || eventType === "done" || eventType === "error" || eventType === "title" || eventType === "billing_error") { - yield { type: eventType, data: parsed.data }; + yield { type: eventType, data: parsed.data, children_id: parsed.children_id }; } } catch { // Ignore unparseable lines @@ -206,3 +260,64 @@ export async function* streamChat(conversationId: string, messageId: string): As } } } + +/** Stream sub-agent output via SSE. + * Connects to `GET /api/ai/subagent/{conversationId}/{childrenId}/stream` + * and yields StreamChunk events (token, thinking, done, error). + */ +export async function* streamSubAgent(conversationId: string, childrenId: string, signal?: AbortSignal): AsyncGenerator { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/subagent/${conversationId}/${childrenId}/stream`, { + method: "GET", + credentials: "include", + signal, + }); + + if (!response.ok) throw new Error(`Sub-agent stream request failed: ${response.status}`); + if (!response.body) throw new Error("No response body"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith("data: ")) continue; + + const jsonStr = trimmed.slice(6); + try { + const parsed = JSON.parse(jsonStr); + const eventType = parsed.type || parsed.event; + if (eventType === "token" || eventType === "thinking" || eventType === "done" || eventType === "stopped" || eventType === "error") { + const data = parsed.data && typeof parsed.data === "object" && "content" in parsed.data + ? parsed.data.content + : parsed.data; + yield { type: eventType, data }; + } else if (eventType === "tool_call" || eventType === "tool_result") { + yield { + type: eventType, + data: parsed.data?.metadata || parsed.data, + children_id: parsed.data?.children_id || parsed.children_id + }; + } + } catch { + // Ignore unparseable lines + } + } + } +} + +export async function stopSubAgent(conversationId: string, childrenId: string): Promise { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/subagent/${conversationId}/${childrenId}/stop`, { + method: "POST", + credentials: "include", + }); + if (!response.ok) throw new Error(`Failed to stop sub-agent: ${response.status}`); +} diff --git a/src/client/api.ts b/src/client/api.ts index fd1a9f0..305bc41 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -15,6 +15,7 @@ const axiosInstance = axios.create({ const api = getApi(axiosInstance); // Export all API functions + export const { // Auth api2faDisable, @@ -67,6 +68,9 @@ export const { projectBilling, projectBillingHistory, projectBillingErrors, + projectMessageFavorites, + projectMessageFavoriteAdd, + projectMessageFavoriteRemove, projectCreateLabel, projectUpdateLabel, projectDeleteLabel, @@ -163,6 +167,8 @@ export const { modelList, categoryList, categoryCreate, + categoryDelete, + categoryUpdate, participantList, pinAdd, pinRemove, @@ -248,6 +254,7 @@ export const { aiMessageCreate, aiMessageGet, aiMessageChildren, + // @ts-ignore aiMessageFork, aiMessageResend, aiMessageStop, @@ -300,6 +307,7 @@ export const { // Search search, + searchMessages, } = api; // Manual avatar upload (not in generated API) diff --git a/src/fonts.css b/src/fonts.css index e69de29..4457eb1 100644 --- a/src/fonts.css +++ b/src/fonts.css @@ -0,0 +1,111 @@ +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Thin.woff2") format("woff2"); + font-weight: 100; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2"); + font-weight: 100; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2"); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2"); + font-weight: 200; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-LightItalic.woff2") format("woff2"); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2"); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-SemiBold.woff2") format("woff2"); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2"); + font-weight: 600; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2"); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2"); + font-weight: 800; + font-style: italic; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index dab0efa..b498ef4 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,12 @@ @plugin "@tailwindcss/typography"; +body { + font-family: Inter, + system-ui, + sans-serif; +} + @custom-variant dark (&:is(.dark *)); @theme inline { @@ -129,192 +135,210 @@ ───────────────────────────────────────────── */ :root { /* Surfaces (layer hierarchy, 3 layers only) */ - --surface-rail: oklch(0.98 0 0); - --surface-sidebar: oklch(0.97 0 0); - --surface-ground: oklch(1 0 0); + --surface-rail: oklch(0.98 0 0); + --surface-sidebar: oklch(0.97 0 0); + --surface-ground: oklch(1 0 0); --surface-elevated: oklch(1 0 0); - --surface-overlay: oklch(1 0 0 / 90%); + --surface-overlay: oklch(1 0 0 / 90%); /* Borders */ - --border-subtle: oklch(0.90 0 0 / 40%); - --border-default: oklch(0.88 0 0); - --border-strong: oklch(0.80 0 0); + --border-subtle: oklch(0.90 0 0 / 40%); + --border-default: oklch(0.88 0 0); + --border-strong: oklch(0.80 0 0); /* Text */ - --text-primary: oklch(0.13 0 0); - --text-secondary: oklch(0.40 0 0); - --text-muted: oklch(0.55 0 0); - --text-inverse: oklch(0.985 0 0); - --text-tertiary: oklch(0.70 0 0); + --text-primary: oklch(0.13 0 0); + --text-secondary: oklch(0.40 0 0); + --text-muted: oklch(0.55 0 0); + --text-inverse: oklch(0.985 0 0); + --text-tertiary: oklch(0.70 0 0); /* Brand accent — Discord blurple as reference */ - --accent: oklch(0.25 0 0); - --accent-hover: oklch(0.15 0 0); - --accent-fg: oklch(0.985 0 0); - --accent-muted: oklch(0.25 0 0 / 15%); - --accent-bg: oklch(0.25 0 0 / 8%); - --accent-rgb: 88, 101, 242; + --accent: oklch(0.25 0 0); + --accent-hover: oklch(0.15 0 0); + --accent-fg: oklch(0.985 0 0); + --accent-muted: oklch(0.25 0 0 / 15%); + --accent-bg: oklch(0.25 0 0 / 8%); + --accent-rgb: 88, 101, 242; /* Status */ - --status-online: oklch(0.55 0 0); - --status-idle: oklch(0.60 0 0); - --status-dnd: oklch(0.50 0 0); - --status-offline: oklch(0.60 0 0); + --status-online: oklch(0.55 0 0); + --status-idle: oklch(0.60 0 0); + --status-dnd: oklch(0.50 0 0); + --status-offline: oklch(0.60 0 0); /* Semantic */ - --success: oklch(0.50 0 0); - --success-alpha10: oklch(0.50 0 0 / 10%); - --warning: oklch(0.60 0 0); - --warning-alpha10: oklch(0.60 0 0 / 10%); - --destructive: oklch(0.45 0 0); + --success: oklch(0.50 0 0); + --success-alpha10: oklch(0.50 0 0 / 10%); + --warning: oklch(0.60 0 0); + --warning-alpha10: oklch(0.60 0 0 / 10%); + --destructive: oklch(0.45 0 0); --destructive-alpha10: oklch(0.45 0 0 / 10%); - --info: oklch(0.50 0 0); + --info: oklch(0.50 0 0); /* Role colors (from Discord) */ - --role-red: oklch(0.50 0 0); - --role-orange: oklch(0.55 0 0); - --role-yellow: oklch(0.60 0 0); - --role-green: oklch(0.50 0 0); - --role-blue: oklch(0.55 0 0); - --role-purple: oklch(0.55 0 0); - --role-pink: oklch(0.55 0 0); - --role-gray: oklch(0.50 0 0); + --role-red: oklch(0.50 0 0); + --role-orange: oklch(0.55 0 0); + --role-yellow: oklch(0.60 0 0); + --role-green: oklch(0.50 0 0); + --role-blue: oklch(0.55 0 0); + --role-purple: oklch(0.55 0 0); + --role-pink: oklch(0.55 0 0); + --role-gray: oklch(0.50 0 0); /* Interactive */ - --interactive: oklch(0.97 0 0); + --interactive: oklch(0.97 0 0); --interactive-hover: oklch(0.92 0 0); --interactive-active: oklch(0.88 0 0); /* Hover */ - --hover-bg: oklch(0.95 0 0 / 70%); - --hover-bg-strong: oklch(0.90 0 0); + --hover-bg: oklch(0.95 0 0 / 70%); + --hover-bg-strong: oklch(0.90 0 0); /* Input */ - --input-bg: oklch(0.98 0 0); + --input-bg: oklch(0.98 0 0); --input-placeholder: oklch(0.50 0 0); - --input-ring: oklch(0.30 0 0); + --input-ring: oklch(0.30 0 0); /* Heatmap (contribution graph) */ - --heatmap-0: oklch(0.93 0 0); - --heatmap-1: oklch(0.75 0.10 155); - --heatmap-2: oklch(0.62 0.15 155); - --heatmap-3: oklch(0.50 0.15 155); - --heatmap-4: oklch(0.38 0.15 155); + --heatmap-0: oklch(0.93 0 0); + --heatmap-1: oklch(0.75 0.10 155); + --heatmap-2: oklch(0.62 0.15 155); + --heatmap-3: oklch(0.50 0.15 155); + --heatmap-4: oklch(0.38 0.15 155); } .dark { - --surface-rail: oklch(0.12 0 0); - --surface-sidebar: oklch(0.15 0 0); - --surface-ground: oklch(0.13 0 0); + --surface-rail: oklch(0.12 0 0); + --surface-sidebar: oklch(0.15 0 0); + --surface-ground: oklch(0.13 0 0); --surface-elevated: oklch(0.18 0 0); - --surface-overlay: oklch(0.10 0 0 / 95%); + --surface-overlay: oklch(0.10 0 0 / 95%); - --border-subtle: oklch(0.30 0 0 / 30%); - --border-default: oklch(0.35 0 0); - --border-strong: oklch(0.50 0 0); + --border-subtle: oklch(0.30 0 0 / 30%); + --border-default: oklch(0.35 0 0); + --border-strong: oklch(0.50 0 0); - --text-primary: oklch(0.97 0 0); - --text-secondary: oklch(0.80 0 0); - --text-muted: oklch(0.65 0 0); - --text-inverse: oklch(0.13 0 0); - --text-tertiary: oklch(0.55 0 0); + --text-primary: oklch(0.97 0 0); + --text-secondary: oklch(0.80 0 0); + --text-muted: oklch(0.65 0 0); + --text-inverse: oklch(0.13 0 0); + --text-tertiary: oklch(0.55 0 0); - --accent: oklch(0.70 0.15 264); - --accent-hover: oklch(0.78 0.15 264); - --accent-fg: oklch(0.10 0 0); - --accent-muted: oklch(0.70 0.15 264 / 20%); - --accent-bg: oklch(0.70 0.15 264 / 12%); - --accent-rgb: 88, 101, 242; + --accent: oklch(0.70 0.15 264); + --accent-hover: oklch(0.78 0.15 264); + --accent-fg: oklch(0.10 0 0); + --accent-muted: oklch(0.70 0.15 264 / 20%); + --accent-bg: oklch(0.70 0.15 264 / 12%); + --accent-rgb: 88, 101, 242; - --status-online: oklch(0.72 0.17 155); - --status-idle: oklch(0.78 0.15 80); - --status-dnd: oklch(0.65 0.20 25); - --status-offline: oklch(0.55 0 0); + --status-online: oklch(0.72 0.17 155); + --status-idle: oklch(0.78 0.15 80); + --status-dnd: oklch(0.65 0.20 25); + --status-offline: oklch(0.55 0 0); - --success: oklch(0.72 0.17 155); - --success-alpha10: oklch(0.72 0.17 155 / 10%); - --warning: oklch(0.78 0.15 90); - --warning-alpha10: oklch(0.78 0.15 90 / 10%); - --destructive: oklch(0.70 0.20 25); + --success: oklch(0.72 0.17 155); + --success-alpha10: oklch(0.72 0.17 155 / 10%); + --warning: oklch(0.78 0.15 90); + --warning-alpha10: oklch(0.78 0.15 90 / 10%); + --destructive: oklch(0.70 0.20 25); --destructive-alpha10: oklch(0.70 0.20 25 / 10%); - --info: oklch(0.70 0.15 250); + --info: oklch(0.70 0.15 250); - --role-red: oklch(0.65 0.20 20); - --role-orange: oklch(0.72 0.18 50); - --role-yellow: oklch(0.78 0.16 85); - --role-green: oklch(0.70 0.17 155); - --role-blue: oklch(0.70 0.20 250); - --role-purple: oklch(0.65 0.20 290); - --role-pink: oklch(0.65 0.20 340); - --role-gray: oklch(0.58 0 0); + --role-red: oklch(0.65 0.20 20); + --role-orange: oklch(0.72 0.18 50); + --role-yellow: oklch(0.78 0.16 85); + --role-green: oklch(0.70 0.17 155); + --role-blue: oklch(0.70 0.20 250); + --role-purple: oklch(0.65 0.20 290); + --role-pink: oklch(0.65 0.20 340); + --role-gray: oklch(0.58 0 0); - --interactive: oklch(0.18 0 0); + --interactive: oklch(0.18 0 0); --interactive-hover: oklch(0.25 0 0); --interactive-active: oklch(0.32 0 0); - --hover-bg: oklch(0.22 0 0 / 60%); - --hover-bg-strong: oklch(0.28 0 0); + --hover-bg: oklch(0.22 0 0 / 60%); + --hover-bg-strong: oklch(0.28 0 0); - --input-bg: oklch(0.15 0 0); + --input-bg: oklch(0.15 0 0); --input-placeholder: oklch(0.55 0 0); - --input-ring: oklch(0.70 0.15 264); + --input-ring: oklch(0.70 0.15 264); /* Heatmap (contribution graph) */ - --heatmap-0: oklch(0.20 0 0); - --heatmap-1: oklch(0.30 0.08 155); - --heatmap-2: oklch(0.45 0.12 155); - --heatmap-3: oklch(0.60 0.15 155); - --heatmap-4: oklch(0.75 0.15 155); + --heatmap-0: oklch(0.20 0 0); + --heatmap-1: oklch(0.30 0.08 155); + --heatmap-2: oklch(0.45 0.12 155); + --heatmap-3: oklch(0.60 0.15 155); + --heatmap-4: oklch(0.75 0.15 155); } @layer base { - * { - @apply border-border outline-ring/50; + * { + @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; + + body { + @apply bg-background text-foreground; } - html { - @apply font-sans; + + html { + @apply font-sans; } } /* ─── Settings Modal open/close animation ─── */ .settings-dialog[data-state="open"] { - animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both; + animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both; } .settings-dialog[data-state="closed"] { - animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both; + animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both; } @keyframes settings-modal-open { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes settings-modal-close { - from { opacity: 1; } - to { opacity: 0; } + from { + opacity: 1; + } + to { + opacity: 0; + } } [data-slot="dialog-overlay"][data-state="open"] { - animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both; + animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both; } [data-slot="dialog-overlay"][data-state="closed"] { - animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both; + animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both; } @keyframes settings-overlay-open { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes settings-overlay-close { - from { opacity: 1; } - to { opacity: 0; } + from { + opacity: 1; + } + to { + opacity: 0; + } } /* ───────────────────────────────────────────── @@ -323,75 +347,81 @@ dark mode text never falls back to defaults ───────────────────────────────────────────── */ :root .prose { - --tw-prose-body: var(--text-primary); - --tw-prose-headings: var(--text-primary); - --tw-prose-lead: var(--text-secondary); - --tw-prose-links: var(--accent); - --tw-prose-bold: var(--text-primary); - --tw-prose-counters: var(--text-secondary); - --tw-prose-bullets: var(--text-muted); - --tw-prose-hr: var(--border-default); - --tw-prose-quotes: var(--text-secondary); - --tw-prose-quote-borders: var(--border-default); - --tw-prose-captions: var(--text-muted); - --tw-prose-kbd: var(--text-primary); - --tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%); - --tw-prose-code: var(--accent); - --tw-prose-pre-code: var(--text-primary); - --tw-prose-pre-bg: var(--surface-elevated); - --tw-prose-th-borders: var(--border-default); - --tw-prose-td-borders: var(--border-subtle); + --tw-prose-body: var(--text-primary); + --tw-prose-headings: var(--text-primary); + --tw-prose-lead: var(--text-secondary); + --tw-prose-links: var(--accent); + --tw-prose-bold: var(--text-primary); + --tw-prose-counters: var(--text-secondary); + --tw-prose-bullets: var(--text-muted); + --tw-prose-hr: var(--border-default); + --tw-prose-quotes: var(--text-secondary); + --tw-prose-quote-borders: var(--border-default); + --tw-prose-captions: var(--text-muted); + --tw-prose-kbd: var(--text-primary); + --tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%); + --tw-prose-code: var(--accent); + --tw-prose-pre-code: var(--text-primary); + --tw-prose-pre-bg: var(--surface-elevated); + --tw-prose-th-borders: var(--border-default); + --tw-prose-td-borders: var(--border-subtle); } .dark .prose { - --tw-prose-body: var(--text-primary); - --tw-prose-headings: var(--text-primary); - --tw-prose-lead: var(--text-secondary); - --tw-prose-links: var(--accent); - --tw-prose-bold: var(--text-primary); - --tw-prose-counters: var(--text-secondary); - --tw-prose-bullets: var(--text-muted); - --tw-prose-hr: var(--border-default); - --tw-prose-quotes: var(--text-secondary); - --tw-prose-quote-borders: var(--border-default); - --tw-prose-captions: var(--text-muted); - --tw-prose-kbd: var(--text-primary); - --tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%); - --tw-prose-code: var(--accent); - --tw-prose-pre-code: var(--text-primary); - --tw-prose-pre-bg: var(--surface-elevated); - --tw-prose-th-borders: var(--border-default); - --tw-prose-td-borders: var(--border-subtle); + --tw-prose-body: var(--text-primary); + --tw-prose-headings: var(--text-primary); + --tw-prose-lead: var(--text-secondary); + --tw-prose-links: var(--accent); + --tw-prose-bold: var(--text-primary); + --tw-prose-counters: var(--text-secondary); + --tw-prose-bullets: var(--text-muted); + --tw-prose-hr: var(--border-default); + --tw-prose-quotes: var(--text-secondary); + --tw-prose-quote-borders: var(--border-default); + --tw-prose-captions: var(--text-muted); + --tw-prose-kbd: var(--text-primary); + --tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%); + --tw-prose-code: var(--accent); + --tw-prose-pre-code: var(--text-primary); + --tw-prose-pre-bg: var(--surface-elevated); + --tw-prose-th-borders: var(--border-default); + --tw-prose-td-borders: var(--border-subtle); } .app-scrollbar { - scrollbar-width: thin; - scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent) transparent; - scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent) transparent; + scrollbar-gutter: stable; } .app-scrollbar::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 10px; + height: 10px; } .app-scrollbar::-webkit-scrollbar-track { - background: transparent; + background: transparent; } .app-scrollbar::-webkit-scrollbar-thumb { - background: color-mix(in oklch, var(--text-muted) 28%, transparent); - border: 3px solid transparent; - border-radius: 999px; - background-clip: padding-box; + background: color-mix(in oklch, var(--text-muted) 28%, transparent); + border: 3px solid transparent; + border-radius: 999px; + background-clip: padding-box; } .app-scrollbar:hover::-webkit-scrollbar-thumb { - background: color-mix(in oklch, var(--text-secondary) 42%, transparent); - border: 2px solid transparent; - background-clip: padding-box; + background: color-mix(in oklch, var(--text-secondary) 42%, transparent); + border: 2px solid transparent; + background-clip: padding-box; } .app-scrollbar[data-scrollbar="room"]::-webkit-scrollbar { - width: 12px; + width: 12px; } + +.pre_nobackground { + pre { + background-color: transparent; + } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 588cc59..d7bc229 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,41 +1,41 @@ import {StrictMode} from "react" import {createRoot} from "react-dom/client" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" - +import {QueryClient, QueryClientProvider} from "@tanstack/react-query" +import "@/fonts.css" import "@/index.css" import {ThemeProvider} from "@/components/theme-provider.tsx" -import { performMaintenance } from "@/lib/db/maintenance"; -import { applyThemePreset } from "@/components/theme/ThemePresetSelector"; +import {Toaster} from "@/components/ui/sonner" +import {performMaintenance} from "@/lib/db/maintenance"; +import {applyThemePreset} from "@/components/theme/ThemePresetSelector"; import App from "@/App.tsx"; -import { initRum } from "@/rum-core"; -import { RumUserContext } from "@/rum"; +import {initRum} from "@/rum-core"; +import {RumUserContext} from "@/rum"; initRum(); -// Apply saved theme preset on startup const PRESET_KEY = "app-theme-preset"; const savedPreset = localStorage.getItem(PRESET_KEY) || "soft-mono"; applyThemePreset(savedPreset); -// Trigger background maintenance on startup performMaintenance().catch(console.error); const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 1, + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, + retry: 1, + }, }, - }, }); createRoot(document.getElementById("root")!).render( - + + -) +) \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index a132313..efd3b72 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -15,6 +15,7 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "resolveJsonModule": true, /* Linting */ "strict": true,