Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ permissions:

jobs:
strix:
timeout-minutes: 90
timeout-minutes: 120
runs-on: ubuntu-latest
steps:
- name: Harden runner
Expand Down Expand Up @@ -311,14 +311,14 @@ jobs:
IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && 'true' || 'false' }}
run: |
budget_suffix="TIME""OUT"
process_budget_seconds="2400"
process_budget_seconds="3600"
if [ "$IS_PR_EVIDENCE_RUN" = "true" ]; then
process_budget_seconds="2400"
process_budget_seconds="3600"
fi
export "LLM_${budget_suffix}=90"
export "LLM_${budget_suffix}=120"
export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=10"
export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds"
export "STRIX_TOTAL_${budget_suffix}_SECONDS=4800"
export "STRIX_TOTAL_${budget_suffix}_SECONDS=7200"
bash "$TRUSTED_STRIX_GATE"

- name: Collect Strix reports for artifact upload
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/app/ai-hub/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const aiHubSurface = {
prompt_cards: [
{
prompt_key: 'prompt_safe',
prompt_title: '의사결정 로그 요약',
prompt_title: '의사결정 로그 맥락 종합',
description_text: '메일에서 판단 포인트를 추출합니다.',
shared_scope: false,
owner_label: 'alice',
Expand All @@ -55,7 +55,7 @@ const aiHubSurface = {
workflow_cards: [
{
workflow_key: 'workflow_prompt_safe',
workflow_title: '의사결정 로그 요약 실행 흐름',
workflow_title: '의사결정 로그 맥락 종합 실행 흐름',
trigger_source: 'prompt_template',
state_code: 'ready',
evidence_text: 'active organization provider is available',
Expand Down Expand Up @@ -164,10 +164,10 @@ describe('AIHubPage', () => {
expect(lowerHeaderNames.has(headerName)).toBe(false);
}
expect(container.textContent).toContain('AI 허브');
expect(container.textContent).toContain('의사결정 로그 요약');
expect(container.textContent).toContain('의사결정 로그 맥락 종합');

clickButton(container, '워크플로우');
expect(container.textContent).toContain('의사결정 로그 요약 실행 흐름');
expect(container.textContent).toContain('의사결정 로그 맥락 종합 실행 흐름');

clickButton(container, 'AI 에이전트');
expect(container.textContent).toContain('Primary OpenAI');
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { metadata } from "./layout";
describe("root layout metadata", () => {
it("describes the Korean-first Naruon workspace and local icons", () => {
expect(metadata.title).toBe("Naruon | AI Email Workspace");
expect(metadata.description).toContain("이메일");
expect(metadata.description).toContain("메일");
expect(metadata.icons).toMatchObject({
icon: "/brand/naruon-app-icon.svg",
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "./globals.css";

export const metadata: Metadata = {
title: "Naruon | AI Email Workspace",
description: "이메일, 일정, 관계, 판단 포인트를 하나의 맥락으로 연결하는 AI 이메일 워크스페이스",
description: "메일, 일정, 관계, 판단 포인트를 하나의 맥락으로 연결하는 AI 메일 워크스페이스",
icons: {
icon: "/brand/naruon-app-icon.svg",
},
Expand Down
16 changes: 8 additions & 8 deletions frontend/src/app/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ describe("Home workspace action bridge", () => {
await flushAsyncWork();

expect(container.querySelector('#mobile-calendar')?.className).toContain("flex");
expect(container.textContent).toContain("캘린더 반영 대기");
expect(container.textContent).toContain("일정 반영 대기");
expect(window.location.hash).toBe("");
});

Expand Down Expand Up @@ -401,7 +401,7 @@ describe("Home workspace action bridge", () => {
ok: true,
json: async () => ({
emails: [
{ id: 102, subject: "출시 리뷰 일정 조율", sender: "pm@example.com", date: "2026-05-17T10:00:00Z", snippet: "캘린더 반영 후보" },
{ id: 102, subject: "출시 리뷰 일정 조율", sender: "pm@example.com", date: "2026-05-17T10:00:00Z", snippet: "일정 반영 후보" },
],
}),
});
Expand Down Expand Up @@ -547,8 +547,8 @@ describe("Home workspace action bridge", () => {
await flushAsyncWork();

expect(container.querySelector('#mobile-calendar')?.className).toContain("flex");
expect(container.textContent).toContain("캘린더 반영 대기");
expect(container.textContent).not.toContain("오늘의 실행 대시보드");
expect(container.textContent).toContain("일정 반영 대기");
expect(container.textContent).not.toContain("오늘의 실행 ");
});

it("lets hashless mobile workspace events override a saved dashboard startup view", async () => {
Expand Down Expand Up @@ -577,8 +577,8 @@ describe("Home workspace action bridge", () => {

expect(window.location.hash).toBe("");
expect(container.querySelector('#mobile-calendar')?.className).toContain("flex");
expect(container.textContent).toContain("캘린더 반영 대기");
expect(container.textContent).not.toContain("오늘의 실행 대시보드");
expect(container.textContent).toContain("일정 반영 대기");
expect(container.textContent).not.toContain("오늘의 실행 ");
});

it("ignores malformed hashless mobile workspace events for dashboard overrides", async () => {
Expand Down Expand Up @@ -682,7 +682,7 @@ describe("Home workspace action bridge", () => {
root?.render(<Home />);
});
await flushAsyncWork();
expect(container.textContent).toContain("캘린더 반영 대기");
expect(container.textContent).toContain("일정 반영 대기");

await act(async () => {
window.history.replaceState(null, "", "/");
Expand All @@ -691,7 +691,7 @@ describe("Home workspace action bridge", () => {
await flushAsyncWork();

expect(container.textContent).toContain("김나루님");
expect(container.textContent).not.toContain("캘린더 반영 대기");
expect(container.textContent).not.toContain("일정 반영 대기");
});

it("renders an API-backed loading state for the mobile search panel instead of placeholders", async () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/prompt-studio/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function PromptStudioPage() {
is_shared: false
});

const [testVariable, setTestVariable] = useState('이메일 내용 예시입니다.');
const [testVariable, setTestVariable] = useState('메일 내용 예시입니다.');
const [testResult, setTestResult] = useState('');
const [testing, setTesting] = useState(false);
const [saving, setSaving] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/tasks/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ describe("TasksPage", () => {
});
await flushAsyncWork();

expect(container.querySelector("h1")?.textContent).toContain("할 일 추적");
expect(container.textContent).toContain("할 일 추적");
expect(container.querySelector("h1")?.textContent).toContain("실행 항목 추적");
expect(container.textContent).toContain("실행 항목 추적");
expect(container.textContent).toContain("위임한 작업");
expect(container.textContent).toContain("실제 티켓 큐");
expect(container.textContent).toContain("연결된 티켓 없음");
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/DashboardLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe("DashboardLayout", () => {
expect(main).not.toBeNull();
expect(skipLink).not.toBeNull();
expect(logo?.getAttribute("src")).toBe("/brand/naruon-symbol.svg");
expect(headerActionButtons).toEqual(["캘린더 반영", "답장 초안", "할 일 만들기"]);
expect(headerActionButtons).toEqual(["일정 반영", "답장 초안", "실행 항목 생성"]);
expect(headerActionGroup?.className).toContain("lg:flex");
expect(headerActionGroup?.className).not.toContain("xl:flex");
expect(skipLink?.textContent).toBe("Skip to main content");
Expand All @@ -103,8 +103,8 @@ describe("DashboardLayout", () => {
const desktopStartupPreference = container.querySelector<HTMLElement>('section[aria-label="Desktop startup preference"]');
expect(desktopStartupPreference).not.toBeNull();
expect(desktopStartupPreference?.textContent ?? "").toContain("시작 화면");
expect(desktopStartupPreference?.querySelector<HTMLButtonElement>('button[data-desktop-startup-view="dashboard"]')?.textContent).toContain("대시보드");
expect(desktopStartupPreference?.querySelector<HTMLButtonElement>('button[data-desktop-startup-view="email"]')?.textContent).toContain("이메일");
expect(desktopStartupPreference?.querySelector<HTMLButtonElement>('button[data-desktop-startup-view="dashboard"]')?.textContent).toContain("");
expect(desktopStartupPreference?.querySelector<HTMLButtonElement>('button[data-desktop-startup-view="email"]')?.textContent).toContain("메일");
expect(desktopStartupPreference?.querySelector<HTMLButtonElement>('button[data-desktop-startup-view="calendar"]')?.textContent).toContain("일정");

act(() => {
Expand All @@ -124,8 +124,8 @@ describe("DashboardLayout", () => {
expect(mobileMenu?.className).toContain("inset-y-0");
expect(mobileMenu?.querySelector<HTMLButtonElement>('button[aria-label="모바일 워크스페이스 메뉴 닫기"]')).not.toBeNull();
expect(mobileMenu?.textContent ?? "").toContain("시작 화면");
expect(mobileMenu?.textContent ?? "").toContain("대시보드");
expect(mobileMenu?.textContent ?? "").toContain("이메일");
expect(mobileMenu?.textContent ?? "").toContain("");
expect(mobileMenu?.textContent ?? "").toContain("메일");
expect(mobileMenu?.textContent ?? "").toContain("일정");
expect(mobileMenu?.textContent ?? "").toContain("메일");
expect(mobileMenu?.textContent ?? "").toContain("워크스페이스");
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,22 @@ const primaryNavItems = [
];

const startupViewItems = [
{ label: '대시보드', view: 'dashboard' as const, description: '오늘의 실행 요약' },
{ label: '이메일', view: 'email' as const, description: '받은편지함 작업공간' },
{ label: '', view: 'dashboard' as const, description: '오늘의 실행 맥락 종합' },
{ label: '메일', view: 'email' as const, description: '받은편지함 작업공간' },
{ label: '일정', view: 'calendar' as const, description: '캘린더 연결 먼저 보기' },
];

const mobileWorkspaceMenuItems = [
{ label: '받은편지함', description: '메일 스레드', icon: Inbox, href: '#mobile-inbox', view: 'inbox' as const },
{ label: '맥락 검색', description: '메일, 첨부, 일정, 사람 검색', icon: Search, href: '#mobile-search', view: 'search' as const },
{ label: '일정 연결', description: '캘린더 반영 후보', icon: CalendarDays, href: '#mobile-calendar', view: 'calendar' as const },
{ label: '일정 연결', description: '일정 반영 후보', icon: CalendarDays, href: '#mobile-calendar', view: 'calendar' as const },
{ label: 'AI 실행', description: '관계 맥락과 실행 항목', icon: Sparkles, href: '#mobile-actions', view: 'actions' as const },
];

const headerActions = [
{ label: '캘린더 반영', action: 'calendar-sync', message: '메일 상세 패널에서 실행 항목을 캘린더에 반영합니다.', icon: CalendarDays },
{ label: '일정 반영', action: 'calendar-sync', message: '메일 상세 패널에서 실행 항목을 캘린더에 반영합니다.', icon: CalendarDays },
{ label: '답장 초안', action: 'reply-draft', message: '메일 상세 패널에서 답장 초안을 생성합니다.', icon: PenLine },
{ label: '할 일 만들기', action: 'create-task', message: '메일 상세 패널에서 실행 항목을 할 일로 정리합니다.', icon: CheckCircle2 },
{ label: '실행 항목 생성', action: 'create-task', message: '메일 상세 패널에서 메일 후속 작업을 실행 항목으로 정리합니다.', icon: CheckCircle2 },
];


Expand Down Expand Up @@ -449,7 +449,7 @@ export function DashboardLayout({
<div className="mb-3 flex items-center justify-between">
<div>
<p className="text-sm font-black text-foreground">워크스페이스 메뉴</p>
<p className="mt-1 text-xs text-muted-foreground">메일, 일정, 할 일, 데이터와 보안 메뉴로 이동합니다.</p>
<p className="mt-1 text-xs text-muted-foreground">메일, 일정, 실행 항목, 데이터와 보안 메뉴로 이동합니다.</p>
</div>
<button
type="button"
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/EmailDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ describe("EmailDetail", () => {
expect(container.textContent).not.toContain("Thread A stale sibling body");
});

it("renders AI summary, action items, and reply drafting in reusable insight cards", async () => {
it("renders 맥락 종합, action items, and reply drafting in reusable insight cards", async () => {
const email: TestEmail = {
id: 7,
message_id: "<insight@example.com>",
Expand Down Expand Up @@ -320,7 +320,7 @@ describe("EmailDetail", () => {
(card) => card.getAttribute("aria-label") === "실행 항목",
);
const createTaskButton = Array.from(actionCard?.querySelectorAll<HTMLButtonElement>("button") ?? []).find(
(button) => button.textContent?.includes("할 일 만들기"),
(button) => button.textContent?.includes("실행 항목 생성"),
);

expect(createTaskButton).not.toBeUndefined();
Expand All @@ -330,7 +330,7 @@ describe("EmailDetail", () => {
});

expect(fetchMock.mock.calls.map(([input]) => String(input))).toContain("/api/tasks/from-email");
expect(actionCard?.textContent).toContain("2개 실행 항목을 티켓형 할 일로 추적합니다.");
expect(actionCard?.textContent).toContain("2개 실행 항목을 티켓형 실행 항목으로 추적합니다.");
});

it("clears conversation loading when the latest email has no thread", async () => {
Expand Down Expand Up @@ -411,7 +411,7 @@ describe("EmailDetail", () => {
expect(container.textContent).not.toContain("대화 흐름을 불러오는 중입니다...");
});

it("uses Korean-first labels for AI summary and execution actions", async () => {
it("uses Korean-first labels for 맥락 종합 and execution actions", async () => {
const email: TestEmail = {
id: 5,
message_id: "<label@example.com>",
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/components/EmailDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
setLlmData(llmJson);
} catch (llmErr) {
console.error("Error generating LLM summary:", llmErr);
if (isMounted) setLlmError("요약을 생성하지 못했습니다.");
if (isMounted) setLlmError("맥락 종합을 생성하지 못했습니다.");
}
} catch (err) {
console.error("Error fetching email details:", err);
Expand Down Expand Up @@ -190,7 +190,7 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
setSendStatus({
type: 'success',
message: data.simulated
? '개발 모드에서 답장을 시뮬레이션했습니다. 실제 이메일은 전송되지 않았습니다.'
? '개발 모드에서 답장을 시뮬레이션했습니다. 실제 메일은 전송되지 않았습니다.'
: '답장을 전송했습니다.',
});
setDraft('');
Expand Down Expand Up @@ -250,10 +250,10 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
items: llmData.todos,
});
if (!isCurrentEmail()) return;
setTaskStatus(`${data.created}개 실행 항목을 티켓형 할 일로 추적합니다.`);
setTaskStatus(`${data.created}개 실행 항목을 티켓형 실행 항목으로 추적합니다.`);
} catch {
if (!isCurrentEmail()) return;
setTaskStatus('티켓형 할 일 생성에 실패했습니다.');
setTaskStatus('티켓형 실행 항목 생성에 실패했습니다.');
} finally {
if (isCurrentEmail()) setIsCreatingTask(false);
}
Expand Down Expand Up @@ -290,7 +290,7 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
<div className="max-w-md rounded-3xl border border-primary/15 bg-card p-8 shadow-[0_24px_70px_rgba(15,23,42,0.08)]">
<div className="mx-auto mb-4 grid size-14 place-items-center rounded-2xl bg-primary/10 text-2xl" aria-hidden="true">✦</div>
<h2 className="text-xl font-black tracking-tight text-foreground">메일을 선택하세요</h2>
<p className="mt-2 text-sm leading-6">왼쪽 받은편지함에서 메일을 선택하면 Naruon이 요약, 판단 포인트, 실행 항목을 연결합니다.</p>
<p className="mt-2 text-sm leading-6">왼쪽 받은편지함에서 메일을 선택하면 Naruon이 맥락 종합, 판단 포인트, 실행 항목을 연결합니다.</p>
</div>
</div>
);
Expand Down Expand Up @@ -367,7 +367,7 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
className="h-9 rounded-xl bg-emerald-600 px-4 text-white hover:bg-emerald-700"
>
{isSyncing && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />}
{isSyncing ? "동기화 중" : "캘린더 반영"}
{isSyncing ? "동기화 중" : "일정 반영"}
</Button>
)}
{llmData.todos.length > 0 && (
Expand All @@ -379,7 +379,7 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
className="h-9 rounded-xl border-emerald-500/30 px-4 text-emerald-700 hover:bg-emerald-500/10"
>
{isCreatingTask && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />}
{isCreatingTask ? "추적 중" : "할 일 만들기"}
{isCreatingTask ? "추적 중" : "실행 항목 생성"}
</Button>
)}
{syncStatus && (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ export function EmailList({
}}
className="flex gap-2"
>
<label htmlFor="email-search" className="sr-only">이메일 검색</label>
<label htmlFor="email-search" className="sr-only">메일 검색</label>
<div className="relative min-w-0 flex-1">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
<Input
id="email-search"
aria-label="이메일 검색"
aria-label="메일 검색"
placeholder="메일, 사람, 키워드 검색..."
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/NetworkGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export default function NetworkGraph() {
{nodes.length}개 노드와 {edges.length}개 관계가 이 스레드 맥락에 연결되어 있습니다.
</p>
<div className="mt-3 rounded-xl border border-primary/10 bg-primary/5 p-3 text-xs text-muted-foreground">
<p className="font-semibold text-foreground">텍스트 관계 요약</p>
<p className="font-semibold text-foreground">텍스트 관계 맥락 종합</p>
<p className="mt-1">
관련 노드: {nodeLabels.join(', ')}
</p>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ProjectsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const projectStatusClass = {
} satisfies Record<ProjectSummary['status'], string>;

const taskStatusLabel: Record<TaskStatus, string> = {
open: '할 일',
open: '실행 항목',
in_progress: '진행 중',
blocked: '검토 필요',
done: '완료',
Expand Down Expand Up @@ -287,7 +287,7 @@ export function ProjectsLayout() {
</div>
<div className="grid gap-4 p-5 md:grid-cols-4">
{[
{ label: '할 일', count: openCount, status: 'open' as const },
{ label: '실행 항목', count: openCount, status: 'open' as const },
{ label: '진행 중', count: inProgressCount, status: 'in_progress' as const },
{ label: '검토 필요', count: blockedCount, status: 'blocked' as const },
{ label: '완료', count: doneCount, status: 'done' as const },
Expand Down
Loading