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
73 changes: 11 additions & 62 deletions src/components/copilot/CopilotPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import {
IconChevronDown,
IconDeviceDesktop,
IconPlayerStop,
IconRobot,
IconTrash,
IconUser,
IconX,
} from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -595,24 +593,7 @@ export function CopilotPanel() {
))}

{/* Header */}
<Group
justify="space-between"
p="xs"
style={{ borderBottom: "1px solid var(--color-border-primary)" }}
>
<Group gap="xs">
<Badge
variant="light"
color="violet"
size="lg"
leftSection={<IconRobot size={12} />}
>
Assistant
</Badge>
<Text size="xs" c="dimmed">
Use @ to mention
</Text>
</Group>
<Group justify="flex-end" p="xs">
<Group gap="xs">
{chatMessages.length > 0 && (
<ActionIcon
Expand Down Expand Up @@ -658,32 +639,19 @@ export function CopilotPanel() {
backgroundColor: "var(--color-bg-secondary)",
}}
>
<Loader size="xs" type="dots" color="var(--color-accent)" />
<Loader size="xs" type="dots" color="var(--color-text-tertiary)" />
<Text size="xs" c="dimmed">
{currentStep === "thinking" && "분석 중..."}
{currentStep === "thinking" && "Analyzing..."}
{currentStep === "tool_call" &&
`${currentToolName || "도구"} 실행 중...`}
{currentStep === "observation" && "결과 처리 중..."}
{currentStep === "final" && "응답 생성 중..."}
`Running ${currentToolName || "tool"}...`}
{currentStep === "observation" && "Processing..."}
{currentStep === "final" && "Generating..."}
</Text>
</Group>
)}

<ScrollArea h="100%" p="md" viewportRef={scrollViewportRef}>
<Stack gap="md">
{chatMessages.length === 0 && (
<Stack
align="center"
justify="center"
h="200px"
style={{ opacity: 0.5 }}
>
<IconRobot size={48} stroke={1.5} />
<Text size="sm" c="dimmed">
How can I help you today?
</Text>
</Stack>
)}
{chatMessages
.filter((msg) => msg.content.trim() !== "")
.map((msg) => {
Expand Down Expand Up @@ -714,24 +682,13 @@ export function CopilotPanel() {
wrap="nowrap"
justify={msg.role === "user" ? "flex-end" : "flex-start"}
>
{msg.role === "assistant" && (
<ActionIcon
variant="light"
color="violet"
radius="xl"
size="sm"
mt={4}
>
<IconRobot size={14} />
</ActionIcon>
)}
<Paper
p="sm"
radius="md"
bg={
msg.role === "user"
? "var(--color-interactive-primary)"
: "var(--color-interactive-selected)"
: "transparent"
}
c={
msg.role === "user"
Expand All @@ -740,7 +697,10 @@ export function CopilotPanel() {
}
style={{
maxWidth: "85%",
border: "none",
border:
msg.role === "user"
? "none"
: "1px solid var(--color-border-primary)",
}}
>
<div
Expand All @@ -759,17 +719,6 @@ export function CopilotPanel() {
}}
/>
</Paper>
{msg.role === "user" && (
<ActionIcon
variant="filled"
color="gray"
radius="xl"
size="sm"
mt={4}
>
<IconUser size={14} />
</ActionIcon>
)}
</Group>
);
})}
Expand Down
51 changes: 24 additions & 27 deletions src/editor/createEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
indentOnInput,
syntaxHighlighting,
} from "@codemirror/language";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { searchKeymap } from "@codemirror/search";
import { EditorState, type Extension } from "@codemirror/state";
import {
EditorView,
Expand Down Expand Up @@ -172,9 +172,6 @@ function createBasicExtensions(config: EditorConfig): Extension[] {
// Auto-close brackets
closeBrackets(),

// Highlight selection matches
highlightSelectionMatches(),

// Keymaps (prepend user bindings so they override defaults)
keymap.of([
// Autocomplete keymap (Enter, Arrows, Esc) must take precedence over
Expand Down Expand Up @@ -309,12 +306,12 @@ function createBasicExtensions(config: EditorConfig): Extension[] {

// Filter out Tab from defaultKeymap to allow block indentation
...defaultKeymap.filter(
(binding) => binding.key !== "Tab" && binding.key !== "Shift-Tab"
(binding) => binding.key !== "Tab" && binding.key !== "Shift-Tab",
),
...historyKeymap,
...closeBracketsKeymap,
...searchKeymap,
])
]),
);

// Line wrapping
Expand Down Expand Up @@ -348,7 +345,7 @@ function normalizeWikiTitle(input: string): string {

function extractBlockRefAtLinePos(
lineText: string,
offsetInLine: number
offsetInLine: number,
): { id: string; isEmbed: boolean } | null {
// Match:
// - ((uuid))
Expand Down Expand Up @@ -471,7 +468,7 @@ function createBlockRefClickHandler(): Extension {
if (!target) return false;

const el = target.closest?.(
".cm-block-ref, .cm-block-embed"
".cm-block-ref, .cm-block-embed",
) as HTMLElement | null;
if (!el) return false;

Expand Down Expand Up @@ -500,7 +497,7 @@ function createBlockRefClickHandler(): Extension {
const lineText = line.text;
const offsetInLine = Math.max(
0,
Math.min(pos - line.from, lineText.length)
Math.min(pos - line.from, lineText.length),
);

const ref = extractBlockRefAtLinePos(lineText, offsetInLine);
Expand All @@ -518,7 +515,7 @@ function createBlockRefClickHandler(): Extension {

function getWikiLinkQueryAtPos(
doc: string,
cursorPos: number
cursorPos: number,
): { from: number; to: number; query: string; isEmbed: boolean } | null {
// Detect an in-progress wiki link like:
// - `[[que` (no closing ]])
Expand Down Expand Up @@ -551,7 +548,7 @@ function getWikiLinkQueryAtPos(

function getParensLinkQueryAtPos(
doc: string,
cursorPos: number
cursorPos: number,
): { from: number; to: number; query: string; isEmbed: boolean } | null {
// Detect in-progress block reference:
// - `((query` -> normal link
Expand Down Expand Up @@ -583,7 +580,7 @@ type PageRecord = { id: string; title: string; parentId?: string };

function computePageFullPathTitles(
pageId: string,
pagesById: Record<string, PageRecord>
pagesById: Record<string, PageRecord>,
): string[] {
const out: string[] = [];
let cur: string | undefined = pageId;
Expand All @@ -605,7 +602,7 @@ function computePageFullPathTitles(

function buildWikiPathForPage(
pageId: string,
pagesById: Record<string, PageRecord>
pagesById: Record<string, PageRecord>,
): string {
return computePageFullPathTitles(pageId, pagesById).join("/");
}
Expand Down Expand Up @@ -655,7 +652,7 @@ function createUnifiedLinkAutocomplete(): Extension {
view: EditorView,
_completion: Completion,
fromPos: number,
toPos: number
toPos: number,
) => {
const state = view.state;
const currentDoc = state.doc.toString();
Expand Down Expand Up @@ -753,7 +750,7 @@ function createUnifiedLinkAutocomplete(): Extension {
_view: EditorView,
_completion: Completion,
_fromPos: number,
_toPos: number
_toPos: number,
) => {
// No-op placeholder; user should keep typing.
},
Expand Down Expand Up @@ -793,7 +790,7 @@ function createUnifiedLinkAutocomplete(): Extension {
view: EditorView,
_completion: Completion,
fromPos: number,
toPos: number
toPos: number,
) => {
const state = view.state;
const currentDoc = state.doc.toString();
Expand Down Expand Up @@ -920,7 +917,7 @@ async function openOrCreateNoteByTitle(noteTitle: string): Promise<void> {
title: string,
parentId: string | null,
pagesById: typeof pageStore.pagesById,
pageIds: string[]
pageIds: string[],
): string | null => {
const t = title.toLowerCase();
for (const id of pageIds) {
Expand All @@ -936,7 +933,7 @@ async function openOrCreateNoteByTitle(noteTitle: string): Promise<void> {
// Ensure a folder-note exists (and isDirectory=true). Return its pageId.
const ensureFolderNote = async (
folderTitle: string,
parentId: string | null
parentId: string | null,
): Promise<string> => {
// Re-read state each time to avoid stale copies after loadPages()
let { pagesById, pageIds } = usePageStore.getState();
Expand All @@ -945,7 +942,7 @@ async function openOrCreateNoteByTitle(noteTitle: string): Promise<void> {
if (!existingId) {
existingId = await pageStore.createPage(
folderTitle,
parentId ?? undefined
parentId ?? undefined,
);
await pageStore.loadPages();
({ pagesById, pageIds } = usePageStore.getState());
Expand Down Expand Up @@ -1016,7 +1013,7 @@ async function openOrCreateNoteByTitle(noteTitle: string): Promise<void> {
}

function createWikiLinkClickHandler(
onOpenWikiLink?: (raw: string, noteTitle: string) => void
onOpenWikiLink?: (raw: string, noteTitle: string) => void,
): Extension {
const handleClick = (event: MouseEvent, view: EditorView) => {
// Only handle left clicks
Expand All @@ -1042,7 +1039,7 @@ function createWikiLinkClickHandler(
// We need to compute the position within the line.
const offsetInLine = Math.max(
0,
Math.min(pos - line.from, lineText.length)
Math.min(pos - line.from, lineText.length),
);

// Scan for wiki links in this line and pick the match that contains the click position.
Expand Down Expand Up @@ -1129,7 +1126,7 @@ function createExternalLinkClickHandler(): Extension {

const offsetInLine = Math.max(
0,
Math.min(pos - line.from, lineText.length)
Math.min(pos - line.from, lineText.length),
);

const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
Expand Down Expand Up @@ -1167,7 +1164,7 @@ function createExternalLinkClickHandler(): Extension {
*/
function createFocusListeners(
onFocus?: () => void,
onBlur?: () => void
onBlur?: () => void,
): Extension[] {
const extensions: Extension[] = [];

Expand All @@ -1178,7 +1175,7 @@ function createFocusListeners(
onFocus();
return false;
},
})
}),
);
}

Expand All @@ -1189,7 +1186,7 @@ function createFocusListeners(
onBlur();
return false;
},
})
}),
);
}

Expand Down Expand Up @@ -1291,7 +1288,7 @@ function createEditorTheme(theme: "light" | "dark" = "light"): Extension {
fontFamily: "var(--font-family)",
},
},
{ dark: isDark }
{ dark: isDark },
);
}

Expand All @@ -1300,7 +1297,7 @@ function createEditorTheme(theme: "light" | "dark" = "light"): Extension {
*/
export function createEditor(
parent: HTMLElement,
config: EditorConfig = {}
config: EditorConfig = {},
): EditorView {
const extensions: Extension[] = [
// Basic extensions
Expand Down
27 changes: 18 additions & 9 deletions src/outliner/BlockComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1294,14 +1294,26 @@ export const BlockComponent: React.FC<BlockComponentProps> = memo(
const view = editorRef.current.getView();
if (view?.hasFocus) {
console.log(
`[BlockComponent:onMouseDown] Committing draft for blockId=${blockId.slice(0, 8)}`,
`[BlockComponent:onMouseDown] Block already focused, updating cursor position for blockId=${blockId.slice(0, 8)}`,
);
commitDraft().then(() => {
console.log(
`[BlockComponent:onMouseDown] Draft committed, blurring blockId=${blockId.slice(0, 8)}`,
const range = document.caretRangeFromPoint(
e.clientX,
e.clientY,
);
if (range && e.currentTarget.contains(range.startContainer)) {
const preCaretRange = document.createRange();
preCaretRange.selectNodeContents(e.currentTarget);
preCaretRange.setEnd(
range.startContainer,
range.startOffset,
);
view.contentDOM.blur();
});
const cursorPos = preCaretRange.toString().length;

view.dispatch({
selection: { anchor: cursorPos, head: cursorPos },
});
view.focus();
}
return;
}
}
Expand All @@ -1310,9 +1322,6 @@ export const BlockComponent: React.FC<BlockComponentProps> = memo(
console.log(
`[BlockComponent:onMouseDown] Setting focus to blockId=${blockId.slice(0, 8)}`,
);
// CRITICAL: Do NOT set targetCursorPosition yet - wait for store to be updated
// If we set it now, other blocks' blockContentEffect might fire before the store update completes
// causing them to override their draft with stale values
setFocusedBlock(blockId);
}
}}
Expand Down