demo
This commit is contained in:
120
src/app/api/chat/route.ts
Normal file
120
src/app/api/chat/route.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import { startChatRun } from "@/lib/chat-runs";
|
||||
import {
|
||||
createChatRunWithMessages,
|
||||
getActiveChatRun,
|
||||
getConversation,
|
||||
getConversationContinuation,
|
||||
getDefaultConversation,
|
||||
getSkill,
|
||||
listMessages,
|
||||
} from "@/lib/db";
|
||||
import { hasDeepSeekApiKey } from "@/lib/deepseek";
|
||||
import type { ChatMessage, Skill } from "@/lib/types";
|
||||
import { createId } from "@/lib/utils";
|
||||
import { chatRequestSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const conversation = getDefaultConversation();
|
||||
const continuation = getConversationContinuation(conversation.id);
|
||||
return Response.json({
|
||||
conversation,
|
||||
messages: listMessages(conversation.id),
|
||||
activeRun: getActiveChatRun(conversation.id),
|
||||
retainedSkillIds: continuation.skillIds,
|
||||
continuation,
|
||||
apiConfigured: hasDeepSeekApiKey(),
|
||||
});
|
||||
} catch (error) {
|
||||
return apiError(error, "无法读取聊天记录");
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const input = chatRequestSchema.parse(await request.json());
|
||||
if (!getConversation(input.conversationId)) {
|
||||
return Response.json({ error: "会话不存在" }, { status: 404 });
|
||||
}
|
||||
const existingMessages = listMessages(input.conversationId);
|
||||
const continuation = getConversationContinuation(
|
||||
input.conversationId,
|
||||
);
|
||||
const selectedSkills = [...new Set(input.selectedSkillIds)]
|
||||
.map((id) => getSkill(id))
|
||||
.filter(
|
||||
(skill): skill is Skill => skill?.status === "active",
|
||||
);
|
||||
|
||||
const userMessageId = createId("message");
|
||||
const assistantMessageId = createId("message");
|
||||
const runId = createId("run");
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const creation = createChatRunWithMessages({
|
||||
conversationId: input.conversationId,
|
||||
userMessage: {
|
||||
id: userMessageId,
|
||||
role: "user",
|
||||
content: input.message,
|
||||
selectedSkillIds: selectedSkills.map((skill) => skill.id),
|
||||
usedSkillIds: [],
|
||||
status: "complete",
|
||||
createdAt: now,
|
||||
},
|
||||
assistantMessage: {
|
||||
id: assistantMessageId,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
selectedSkillIds: selectedSkills.map((skill) => skill.id),
|
||||
usedSkillIds: [],
|
||||
status: "streaming",
|
||||
createdAt: new Date(Date.now() + 1).toISOString(),
|
||||
},
|
||||
run: {
|
||||
id: runId,
|
||||
clientRequestId: input.clientRequestId,
|
||||
userMessageId,
|
||||
assistantMessageId,
|
||||
},
|
||||
});
|
||||
if (creation.outcome === "existing") {
|
||||
return Response.json({ run: creation.run });
|
||||
}
|
||||
if (creation.outcome === "active_conflict") {
|
||||
return Response.json(
|
||||
{ error: "当前已有一轮回答正在进行", run: creation.run },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
const run = creation.run;
|
||||
|
||||
const history = existingMessages
|
||||
.filter((message) => message.status !== "error")
|
||||
.slice(-20)
|
||||
.map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})) satisfies Array<{
|
||||
role: ChatMessage["role"];
|
||||
content: string;
|
||||
}>;
|
||||
|
||||
startChatRun({
|
||||
runId,
|
||||
conversationId: input.conversationId,
|
||||
userMessage: input.message,
|
||||
selectedSkills,
|
||||
continuation,
|
||||
history,
|
||||
});
|
||||
|
||||
return Response.json({ run }, { status: 202 });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法发送消息");
|
||||
}
|
||||
}
|
||||
22
src/app/api/chat/runs/[id]/stop/route.ts
Normal file
22
src/app/api/chat/runs/[id]/stop/route.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { stopChatRun } from "@/lib/chat-runs";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
context: RouteContext<"/api/chat/runs/[id]/stop">,
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
const payload = (await request.json().catch(() => null)) as {
|
||||
visibleContent?: unknown;
|
||||
} | null;
|
||||
const visibleContent =
|
||||
typeof payload?.visibleContent === "string"
|
||||
? payload.visibleContent.slice(0, 200_000)
|
||||
: undefined;
|
||||
const result = await stopChatRun(id, { visibleContent });
|
||||
if (!result.found) {
|
||||
return Response.json({ error: "回答任务不存在" }, { status: 404 });
|
||||
}
|
||||
return Response.json(result);
|
||||
}
|
||||
51
src/app/api/chat/runs/[id]/stream/route.ts
Normal file
51
src/app/api/chat/runs/[id]/stream/route.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { getChatRun, listChatRunEvents } from "@/lib/db";
|
||||
import { encodeSse, sseResponse } from "@/lib/sse";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
|
||||
const terminalStatuses = new Set(["complete", "stopped", "error"]);
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
context: RouteContext<"/api/chat/runs/[id]/stream">,
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
if (!getChatRun(id)) {
|
||||
return Response.json({ error: "回答任务不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
let cursor = Math.max(0, Number(url.searchParams.get("after") ?? "0") || 0);
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
try {
|
||||
while (!request.signal.aborted) {
|
||||
const events = listChatRunEvents(id, cursor);
|
||||
for (const event of events) {
|
||||
controller.enqueue(encodeSse(event));
|
||||
cursor = Math.max(cursor, event.seq ?? cursor);
|
||||
}
|
||||
|
||||
const run = getChatRun(id);
|
||||
if (!run || terminalStatuses.has(run.status)) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 140));
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
// The browser disconnected between polling cycles.
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return sseResponse(stream);
|
||||
}
|
||||
|
||||
32
src/app/api/conversation/route.ts
Normal file
32
src/app/api/conversation/route.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import {
|
||||
getDefaultConversation,
|
||||
renameConversation,
|
||||
resetConversation,
|
||||
} from "@/lib/db";
|
||||
import { renameConversationSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
try {
|
||||
const input = renameConversationSchema.parse(await request.json());
|
||||
const conversation = getDefaultConversation();
|
||||
const updated = renameConversation(conversation.id, input.title);
|
||||
return Response.json({ conversation: updated });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法修改会话标题");
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const conversation = getDefaultConversation();
|
||||
return Response.json({
|
||||
conversation: resetConversation(conversation.id),
|
||||
});
|
||||
} catch (error) {
|
||||
return apiError(error, "无法重置会话");
|
||||
}
|
||||
}
|
||||
|
||||
96
src/app/api/skills/[id]/route.ts
Normal file
96
src/app/api/skills/[id]/route.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import { deleteSkill, getSkill, saveSkill } from "@/lib/db";
|
||||
import {
|
||||
getSkillCapabilityViolations,
|
||||
skillCapabilityErrorMessage,
|
||||
} from "@/lib/skill-capabilities";
|
||||
import {
|
||||
getSkillNodeQualityIssues,
|
||||
skillNodeQualityErrorMessage,
|
||||
} from "@/lib/skill-node-quality";
|
||||
import { saveSkillSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
context: RouteContext<"/api/skills/[id]">,
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const skill = getSkill(id);
|
||||
|
||||
if (!skill) {
|
||||
return Response.json({ error: "Skill 不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({ skill });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法读取 Skill");
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
context: RouteContext<"/api/skills/[id]">,
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
if (!getSkill(id)) {
|
||||
return Response.json({ error: "Skill 不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const input = saveSkillSchema.parse({
|
||||
...(await request.json()),
|
||||
id,
|
||||
});
|
||||
const capabilityViolations = getSkillCapabilityViolations(input);
|
||||
if (capabilityViolations.length > 0) {
|
||||
return Response.json(
|
||||
{ error: skillCapabilityErrorMessage(capabilityViolations) },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(input.status ?? "active") === "active" &&
|
||||
input.nodes.some((node) => !node.completed)
|
||||
) {
|
||||
return Response.json(
|
||||
{ error: "五个节点全部完成后才能保存 Skill" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
if ((input.status ?? "active") === "active") {
|
||||
const qualityIssues = getSkillNodeQualityIssues(input.nodes);
|
||||
if (qualityIssues.length > 0) {
|
||||
return Response.json(
|
||||
{ error: skillNodeQualityErrorMessage(qualityIssues) },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ skill: saveSkill(input) });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法更新 Skill");
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
context: RouteContext<"/api/skills/[id]">,
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const deleted = deleteSkill(id);
|
||||
|
||||
if (!deleted) {
|
||||
return Response.json({ error: "Skill 不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法删除 Skill");
|
||||
}
|
||||
}
|
||||
15
src/app/api/skills/builder/[id]/stop/route.ts
Normal file
15
src/app/api/skills/builder/[id]/stop/route.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { stopBuilderRun } from "@/lib/builder-runs";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
context: RouteContext<"/api/skills/builder/[id]/stop">,
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
if (!stopBuilderRun(id)) {
|
||||
return Response.json({ error: "节点任务不存在或已过期" }, { status: 404 });
|
||||
}
|
||||
return Response.json({ stopped: true });
|
||||
}
|
||||
|
||||
60
src/app/api/skills/builder/route.ts
Normal file
60
src/app/api/skills/builder/route.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import {
|
||||
getBuilderRunEvents,
|
||||
getOrStartBuilderRun,
|
||||
} from "@/lib/builder-runs";
|
||||
import { encodeSse, sseResponse } from "@/lib/sse";
|
||||
import { builderRequestSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
|
||||
const terminalStatuses = new Set(["complete", "stopped", "error"]);
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const input = builderRequestSchema.parse(await request.json());
|
||||
const run = getOrStartBuilderRun(input);
|
||||
let cursor = input.afterSeq;
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
try {
|
||||
while (!request.signal.aborted) {
|
||||
const snapshot = getBuilderRunEvents(run.id, cursor);
|
||||
if (!snapshot) {
|
||||
controller.enqueue(
|
||||
encodeSse({ type: "error", message: "节点任务已过期" }),
|
||||
);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of snapshot.events) {
|
||||
controller.enqueue(encodeSse(event));
|
||||
cursor = Math.max(cursor, event.seq ?? cursor);
|
||||
}
|
||||
|
||||
if (terminalStatuses.has(snapshot.status)) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
// A disconnected editor can reconnect with the last event seq.
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return sseResponse(stream);
|
||||
} catch (error) {
|
||||
return apiError(error, "无法处理 Skill 对话");
|
||||
}
|
||||
}
|
||||
|
||||
15
src/app/api/skills/builder/suggestions/route.ts
Normal file
15
src/app/api/skills/builder/suggestions/route.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import { generateBuilderSuggestions } from "@/lib/deepseek";
|
||||
import { builderSuggestionsRequestSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const input = builderSuggestionsRequestSchema.parse(await request.json());
|
||||
return Response.json(await generateBuilderSuggestions(input));
|
||||
} catch (error) {
|
||||
return apiError(error, "无法生成节点建议");
|
||||
}
|
||||
}
|
||||
58
src/app/api/skills/route.ts
Normal file
58
src/app/api/skills/route.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { apiError } from "@/lib/api";
|
||||
import { listSkills, saveSkill } from "@/lib/db";
|
||||
import {
|
||||
getSkillCapabilityViolations,
|
||||
skillCapabilityErrorMessage,
|
||||
} from "@/lib/skill-capabilities";
|
||||
import {
|
||||
getSkillNodeQualityIssues,
|
||||
skillNodeQualityErrorMessage,
|
||||
} from "@/lib/skill-node-quality";
|
||||
import { saveSkillSchema } from "@/lib/validation";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return Response.json({ skills: listSkills() });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法读取 Skill");
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const input = saveSkillSchema.parse(await request.json());
|
||||
const capabilityViolations = getSkillCapabilityViolations(input);
|
||||
if (capabilityViolations.length > 0) {
|
||||
return Response.json(
|
||||
{ error: skillCapabilityErrorMessage(capabilityViolations) },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(input.status ?? "active") === "active" &&
|
||||
input.nodes.some((node) => !node.completed)
|
||||
) {
|
||||
return Response.json(
|
||||
{ error: "五个节点全部完成后才能保存 Skill" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
if ((input.status ?? "active") === "active") {
|
||||
const qualityIssues = getSkillNodeQualityIssues(input.nodes);
|
||||
if (qualityIssues.length > 0) {
|
||||
return Response.json(
|
||||
{ error: skillNodeQualityErrorMessage(qualityIssues) },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const skill = saveSkill(input);
|
||||
return Response.json({ skill }, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiError(error, "无法保存 Skill");
|
||||
}
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
277
src/app/globals.css
Normal file
277
src/app/globals.css
Normal file
@ -0,0 +1,277 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-shimmer";
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: #fafafa;
|
||||
--foreground: #18181b;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #18181b;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #18181b;
|
||||
--primary: #18181b;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #f4f4f5;
|
||||
--secondary-foreground: #27272a;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #71717a;
|
||||
--accent: #f4f4f5;
|
||||
--accent-foreground: #18181b;
|
||||
--destructive: #dc2626;
|
||||
--border: #e4e4e7;
|
||||
--input: #e4e4e7;
|
||||
--ring: #a1a1aa;
|
||||
|
||||
--canvas: var(--background);
|
||||
--surface: #ffffff;
|
||||
--ink: var(--foreground);
|
||||
--slate: var(--muted-foreground);
|
||||
--line: var(--border);
|
||||
--cobalt: #18181b;
|
||||
--cobalt-soft: #f4f4f5;
|
||||
--mint: #16a34a;
|
||||
--mint-dark: #166534;
|
||||
--mint-soft: #f0fdf4;
|
||||
--coral: var(--destructive);
|
||||
--coral-soft: #fef2f2;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-canvas: var(--canvas);
|
||||
--color-surface: var(--surface);
|
||||
--color-ink: var(--ink);
|
||||
--color-line: var(--line);
|
||||
--color-cobalt: var(--cobalt);
|
||||
--color-cobalt-soft: var(--cobalt-soft);
|
||||
--color-mint: var(--mint);
|
||||
--color-mint-dark: var(--mint-dark);
|
||||
--color-mint-soft: var(--mint-soft);
|
||||
--color-coral: var(--coral);
|
||||
--color-coral-soft: var(--coral-soft);
|
||||
--font-sans:
|
||||
"Manrope Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI",
|
||||
sans-serif;
|
||||
--font-display:
|
||||
"Manrope Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI",
|
||||
sans-serif;
|
||||
--font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
|
||||
@keyframes collapsible-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(
|
||||
--radix-collapsible-content-height,
|
||||
var(--collapsible-panel-height, auto)
|
||||
);
|
||||
}
|
||||
}
|
||||
@keyframes collapsible-up {
|
||||
from {
|
||||
height: var(
|
||||
--radix-collapsible-content-height,
|
||||
var(--collapsible-panel-height, auto)
|
||||
);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--canvas);
|
||||
color: var(--ink);
|
||||
font-family:
|
||||
"Manrope Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(59, 130, 246, 0.28);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: rgba(113, 113, 122, 0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(113, 113, 122, 0.34);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.markdown > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 0.62em 0;
|
||||
}
|
||||
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3 {
|
||||
margin: 1.15em 0 0.42em;
|
||||
font-family:
|
||||
"Manrope Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI",
|
||||
sans-serif;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 1.16em;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 1.04em;
|
||||
}
|
||||
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
margin: 0.65em 0;
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
|
||||
.markdown li {
|
||||
margin: 0.22em 0;
|
||||
}
|
||||
|
||||
.markdown li::marker {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
margin: 0.8em 0;
|
||||
border-left: 2px solid var(--border);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: var(--muted);
|
||||
padding: 0.5em 0.8em;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
border-radius: 5px;
|
||||
background: #eef1f5;
|
||||
padding: 0.14em 0.36em;
|
||||
font-family: "IBM Plex Mono", Consolas, monospace;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.markdown pre {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
background: #18181b;
|
||||
padding: 1em;
|
||||
color: #eef2fb;
|
||||
}
|
||||
|
||||
.markdown pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.markdown table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.markdown th,
|
||||
.markdown td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.45em 0.7em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown th {
|
||||
background: var(--canvas);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-open (&:where([data-state="open"], [data-open]:not([data-open="false"])));
|
||||
|
||||
@custom-variant data-closed (&:where([data-state="closed"], [data-closed]:not([data-closed="false"])));
|
||||
27
src/app/layout.tsx
Normal file
27
src/app/layout.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import "@fontsource-variable/manrope";
|
||||
import "@fontsource/ibm-plex-mono/500.css";
|
||||
import "@fontsource/ibm-plex-mono/600.css";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Skill Loom · AI Skill 工作台",
|
||||
template: "%s · Skill Loom",
|
||||
},
|
||||
description:
|
||||
"组合、创建并在单轮 AI 对话中智能调用可复用 Skill 的工作台。",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" className="h-full">
|
||||
<body className="min-h-full">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
6
src/app/page.tsx
Normal file
6
src/app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { ChatWorkspace } from "@/components/chat-workspace";
|
||||
|
||||
export default function Home() {
|
||||
return <ChatWorkspace />;
|
||||
}
|
||||
|
||||
9
src/app/skills/[id]/page.tsx
Normal file
9
src/app/skills/[id]/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { SkillBuilder } from "@/components/skill-builder";
|
||||
|
||||
export default async function EditSkillPage(
|
||||
props: PageProps<"/skills/[id]">,
|
||||
) {
|
||||
const { id } = await props.params;
|
||||
return <SkillBuilder skillId={id} />;
|
||||
}
|
||||
|
||||
6
src/app/skills/new/page.tsx
Normal file
6
src/app/skills/new/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { SkillBuilder } from "@/components/skill-builder";
|
||||
|
||||
export default function NewSkillPage() {
|
||||
return <SkillBuilder />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user