diff --git a/app/app/globals.css b/app/app/globals.css index c7ff145..1db8c24 100644 --- a/app/app/globals.css +++ b/app/app/globals.css @@ -1,5 +1,46 @@ :root { + --bg-primary: #ffffff; + --bg-page: linear-gradient(140deg, #f9fafb 0%, #e7f1ff 45%, #fff7f0 100%); + --bg-card: rgba(255, 255, 255, 0.88); + --bg-code: #0b1020; + --bg-message: #eff6ff; + --text-primary: #1f2937; + --text-secondary: #4b5563; + --text-code: #e5edff; + --border-card: #d9e4f2; + --border-input: #b9c6db; + --border-message: #bfdbfe; + --accent: #1d4ed8; + --accent-hover: #1e40af; + --accent-active: #1e3a8a; + --accent-light: #3b82f6; + --accent-disabled: #93c5fd; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + line-height: 1.6; + letter-spacing: -0.01em; + color: var(--text-primary); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #111827; + --bg-page: linear-gradient(140deg, #111827 0%, #1e293b 45%, #1a1a2e 100%); + --bg-card: rgba(30, 41, 59, 0.88); + --bg-code: #0b1020; + --bg-message: #1e293b; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --text-code: #e5edff; + --border-card: #334155; + --border-input: #475569; + --border-message: #334155; + --accent: #3b82f6; + --accent-hover: #60a5fa; + --accent-active: #2563eb; + --accent-light: #60a5fa; + --accent-disabled: #1e3a8a; + } } * { @@ -8,8 +49,9 @@ body { margin: 0; - background: linear-gradient(140deg, #f9fafb 0%, #e7f1ff 45%, #fff7f0 100%); - color: #1f2937; + background: var(--bg-page); + color: var(--text-primary); + font-size: 1rem; } main { @@ -20,6 +62,24 @@ main { h1 { margin-top: 0; + font-size: 1.75rem; + letter-spacing: -0.02em; +} + +h2 { + font-size: 1.25rem; + margin-top: 0; +} + +h3 { + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.subtitle { + color: var(--text-secondary); + margin-top: -0.5rem; + margin-bottom: 1.5rem; } .grid { @@ -29,8 +89,8 @@ h1 { } .card { - background: rgba(255, 255, 255, 0.88); - border: 1px solid #d9e4f2; + background: var(--bg-card); + border: 1px solid var(--border-card); border-radius: 14px; padding: 1rem; } @@ -41,77 +101,115 @@ h1 { margin-bottom: 0.8rem; } +.form-field label { + display: flex; + justify-content: space-between; + align-items: baseline; + font-weight: 500; + font-size: 0.875rem; +} + +.char-count { + font-size: 0.75rem; + font-weight: 400; + color: var(--text-secondary); +} + input, textarea, select { - border: 1px solid #b9c6db; + border: 1px solid var(--border-input); border-radius: 8px; padding: 0.65rem; + background: var(--bg-primary); + color: var(--text-primary); + font-family: inherit; + font-size: 0.9375rem; + line-height: 1.5; transition: border-color 0.15s, outline-color 0.15s; } +textarea { + resize: vertical; + min-height: 3rem; +} + input:focus, textarea:focus, select:focus { - border-color: #3b82f6; - outline: 2px solid #3b82f6; + border-color: var(--accent-light); + outline: 2px solid var(--accent-light); outline-offset: -1px; } button { - background: #1d4ed8; + background: var(--accent); border: none; color: #fff; border-radius: 8px; padding: 0.65rem 1rem; cursor: pointer; + font-family: inherit; + font-size: 0.9375rem; + font-weight: 500; transition: background 0.15s; } button:hover { - background: #1e40af; + background: var(--accent-hover); } button:active { - background: #1e3a8a; + background: var(--accent-active); } button:focus-visible { - outline: 2px solid #3b82f6; + outline: 2px solid var(--accent-light); outline-offset: 2px; } button:disabled { - background: #93c5fd; + background: var(--accent-disabled); cursor: not-allowed; } pre { white-space: pre-wrap; - background: #0b1020; - color: #e5edff; + background: var(--bg-code); + color: var(--text-code); padding: 0.75rem; border-radius: 10px; + font-size: 0.875rem; + line-height: 1.5; } .message { - background: #eff6ff; - border: 1px solid #bfdbfe; + background: var(--bg-message); + border: 1px solid var(--border-message); border-radius: 8px; padding: 0.75rem 1rem; margin-top: 1rem; } +.answer { + line-height: 1.7; +} + +.answer p { + margin: 0.5rem 0; +} + .meta { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.75rem; margin: 0.5rem 0; + font-size: 0.875rem; } .meta dt { font-weight: 600; - color: #4b5563; + color: var(--text-secondary); } .meta dd { @@ -126,4 +224,102 @@ pre { .citations li { margin-bottom: 0.5rem; line-height: 1.5; + font-size: 0.875rem; +} + +.badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.badge-ready { + background: #dcfce7; + color: #166534; +} + +.badge-processing { + background: #fef9c3; + color: #854d0e; +} + +.badge-error { + background: #fce4ec; + color: #b71c1c; +} + +@media (prefers-color-scheme: dark) { + .badge-ready { + background: #14532d; + color: #86efac; + } + .badge-processing { + background: #713f12; + color: #fde047; + } + .badge-error { + background: #7f1d1d; + color: #fca5a5; + } +} + +.doc-list { + list-style: none; + padding: 0; + margin: 0.5rem 0; +} + +.doc-list li { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-card); + font-size: 0.875rem; +} + +.doc-list li:last-child { + border-bottom: none; +} + +.doc-name { + font-weight: 500; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.doc-chunks { + color: var(--text-secondary); + font-size: 0.75rem; +} + +.empty { + color: var(--text-secondary); + font-style: italic; + text-align: center; + padding: 1rem 0; +} + +@media (max-width: 640px) { + main { + padding: 1rem 0.75rem 3rem; + } + + h1 { + font-size: 1.5rem; + } + + .grid { + grid-template-columns: 1fr; + } +} + +@media (min-width: 641px) and (max-width: 960px) { + main { + padding: 1.5rem 1rem 3rem; + } } diff --git a/app/app/page.tsx b/app/app/page.tsx index cfa761c..47b6389 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { FormEvent, useEffect, useState } from "react"; +import { FormEvent, useEffect, useRef, useState } from "react"; +import Markdown from "react-markdown"; import { ENDPOINT, fetchApi, ApiError } from "./lib"; type DocumentItem = { @@ -43,6 +44,18 @@ function errorMessage(error: unknown): string { return "네트워크 오류가 발생했습니다. 잠시 후 다시 시도하세요."; } +const SOURCE_MAX = 5000; + +function statusBadge(status: string) { + const cls = + status === "ready" + ? "badge badge-ready" + : status === "processing" + ? "badge badge-processing" + : "badge badge-error"; + return {status}; +} + export default function HomePage() { const [documents, setDocuments] = useState([]); const [metrics, setMetrics] = useState(null); @@ -53,11 +66,20 @@ export default function HomePage() { const [actionResult, setActionResult] = useState(""); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(false); + const sourceRef = useRef(null); + const questionRef = useRef(null); useEffect(() => { void refreshAll(); }, []); + function autoResize(ref: React.RefObject) { + const el = ref.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + } + async function refreshAll() { try { const [docs, metricPayload] = await Promise.all([ @@ -145,7 +167,7 @@ export default function HomePage() { return (

Knowledge Copilot

-

RAG, 액션형 AI, API 로그 추적까지 포함한 포트폴리오 프로젝트

+

RAG, 액션형 AI, API 로그 추적까지 포함한 포트폴리오 프로젝트

@@ -160,12 +182,22 @@ export default function HomePage() { />
- +