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
2 changes: 0 additions & 2 deletions src/app/api/cards/from-message/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const userId = session.user.id;

const body = await request.json();
const { content, workspaceId, folderId, sources } = body;

Expand Down
151 changes: 88 additions & 63 deletions src/app/api/workspaces/autogen/route.ts

Large diffs are not rendered by default.

42 changes: 18 additions & 24 deletions src/components/assistant-ui/CreateQuizToolUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,9 @@ import { initialState } from "@/lib/workspace-state/state";
import { ToolUIErrorBoundary } from "@/components/tool-ui/shared";
import type { QuizResult } from "@/lib/ai/tool-result-schemas";
import { parseQuizResult } from "@/lib/ai/tool-result-schemas";

type CreateQuizArgs = {
topic?: string;
difficulty?: "easy" | "medium" | "hard";
contextContent?: string;
sourceCardIds?: string[];
sourceCardNames?: string[];
};
import type { CreateQuizInput } from "@/lib/ai/tools/quiz-tools";

interface CreateQuizReceiptProps {
args: CreateQuizArgs;
result: QuizResult;
status: any;
moveItemToFolder?: (itemId: string, folderId: string | null) => void;
Expand All @@ -41,26 +33,31 @@ interface CreateQuizReceiptProps {
workspaceColor?: string | null;
}

const difficultyColors = {
easy: "bg-green-500/10 text-green-600",
medium: "bg-yellow-500/10 text-yellow-600",
hard: "bg-red-500/10 text-red-600",
};

const CreateQuizReceipt = ({ args, result, status, moveItemToFolder, allItems = [], workspaceName = "Workspace", workspaceIcon, workspaceColor }: CreateQuizReceiptProps) => {
const CreateQuizReceipt = ({ result, status, moveItemToFolder, allItems = [], workspaceName = "Workspace", workspaceIcon, workspaceColor }: CreateQuizReceiptProps) => {
const workspaceId = useWorkspaceStore((state) => state.currentWorkspaceId);
const { state: workspaceState } = useWorkspaceState(workspaceId);
const navigateToItem = useNavigateToItem();

// State for MoveToDialog
const [showMoveDialog, setShowMoveDialog] = useState(false);

// Get the current item from workspace state
// Get the current item — try workspace state first, then allItems, then stub from result
const currentItem = useMemo(() => {
const targetId = result.itemId || result.quizId;
if (!targetId || !workspaceState?.items) return undefined;
return workspaceState.items.find((item: any) => item.id === targetId);
}, [result.itemId, result.quizId, workspaceState?.items]);
if (!targetId) return undefined;
const fromWorkspace = workspaceState?.items?.find((item: { id: string }) => item.id === targetId);
if (fromWorkspace) return fromWorkspace;
const fromAll = allItems.find((item: { id: string }) => item.id === targetId);
if (fromAll) return fromAll;
return {
id: targetId,
name: (result as { title?: string }).title ?? "Quiz",
type: "quiz" as const,
subtitle: "",
data: {},
folderId: undefined,
};
}, [result.itemId, result.quizId, result, workspaceState?.items, allItems]);

// Get folder name if item is in a folder
const folderName = useMemo(() => {
Expand All @@ -82,8 +79,6 @@ const CreateQuizReceipt = ({ args, result, status, moveItemToFolder, allItems =
}
};

const difficulty = result.difficulty || args.difficulty || "medium";

return (
<>
<div
Expand Down Expand Up @@ -167,7 +162,7 @@ const CreateQuizReceipt = ({ args, result, status, moveItemToFolder, allItems =
);
};

export const CreateQuizToolUI = makeAssistantToolUI<CreateQuizArgs, QuizResult>({
export const CreateQuizToolUI = makeAssistantToolUI<CreateQuizInput, QuizResult>({
toolName: "createQuiz",
render: function CreateQuizUI({ args, result, status }) {
const workspaceId = useWorkspaceStore((state) => state.currentWorkspaceId);
Expand Down Expand Up @@ -201,7 +196,6 @@ export const CreateQuizToolUI = makeAssistantToolUI<CreateQuizArgs, QuizResult>(
if (parsed?.success) {
content = (
<CreateQuizReceipt
args={args}
result={parsed}
status={status}
moveItemToFolder={operations.moveItemToFolder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useWorkspaceState } from "@/hooks/workspace/use-workspace-state";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useOptimisticToolUpdate } from "@/hooks/ai/use-optimistic-tool-update";
import { X, Eye } from "lucide-react";
import { CgNotes } from "react-icons/cg";
import { Pencil } from "lucide-react";
import { useWorkspaceStore } from "@/lib/stores/workspace-store";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
Expand All @@ -18,42 +18,55 @@ import { ToolUIErrorShell } from "@/components/assistant-ui/tool-ui-error-shell"
import { DiffViewer } from "@/components/assistant-ui/diff-viewer";
import type { WorkspaceResult } from "@/lib/ai/tool-result-schemas";
import { parseWorkspaceResult } from "@/lib/ai/tool-result-schemas";
import type { Item } from "@/lib/workspace-state/types";

type EditItemArgs = {
itemName: string;
oldString: string;
newString: string;
replaceAll?: boolean;
newName?: string;
};

type UpdateNoteArgs = { noteName: string; oldString: string; newString: string; replaceAll?: boolean };

interface UpdateNoteResult extends WorkspaceResult {
interface EditItemResult extends WorkspaceResult {
diff?: string;
filediff?: { additions: number; deletions: number };
cardCount?: number;
questionCount?: number;
}

interface UpdateNoteReceiptProps {
args: UpdateNoteArgs;
result: UpdateNoteResult;
status: any;
interface EditItemReceiptProps {
args: EditItemArgs;
result: EditItemResult;
status: { type: string };
}

const UpdateNoteReceipt = ({ args, result, status }: UpdateNoteReceiptProps) => {
const EditItemReceipt = ({ args, result, status }: EditItemReceiptProps) => {
const setOpenModalItemId = useUIStore((s) => s.setOpenModalItemId);
const workspaceId = useWorkspaceStore((s) => s.currentWorkspaceId);
const { state: workspaceState } = useWorkspaceState(workspaceId);
const navigateToItem = useNavigateToItem();

// Get the card from workspace to show its title
const card = useMemo(() => {
if (!result.itemId || !workspaceState?.items) return null;
return workspaceState.items.find((item: any) => item.id === result.itemId);
if (!result.itemId || !workspaceState?.items) return undefined;
return workspaceState.items.find((item: Item) => item.id === result.itemId);
}, [result.itemId, workspaceState?.items]);

const handleViewCard = () => {
if (!result.itemId) return;
// Only open modal if item exists and navigation succeeds
if (navigateToItem(result.itemId)) {
setOpenModalItemId(result.itemId);
}
};

const hasDiff = result.diff && result.diff.trim().length > 0;

const subtitle = useMemo(() => {
if (result.cardCount != null) return `${result.cardCount} cards`;
if (result.questionCount != null) return `${result.questionCount} questions`;
return "Item updated";
}, [result.cardCount, result.questionCount]);

return (
<div className="my-1 flex flex-col gap-2">
<div
Expand All @@ -64,22 +77,20 @@ const UpdateNoteReceipt = ({ args, result, status }: UpdateNoteReceiptProps) =>
onClick={status?.type === "complete" && result.itemId ? handleViewCard : undefined}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className={cn(status?.type === "complete" ? "text-blue-400" : "text-red-400")}
>
<div className={cn(status?.type === "complete" ? "text-blue-400" : "text-red-400")}>
{status?.type === "complete" ? (
<CgNotes className="size-4" />
<Pencil className="size-4" />
) : (
<X className="size-4" />
)}
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-xs font-medium truncate">
{status?.type === "complete" ? (card?.name || "Card Updated") : "Update Cancelled"}
{status?.type === "complete"
? String(card?.name ?? (result as { itemName?: string }).itemName ?? "Item Updated")
: "Edit Cancelled"}
</span>
{status?.type === "complete" && (
<span className="text-[10px] text-muted-foreground">Note updated</span>
)}
{status?.type === "complete" && <span className="text-[10px] text-muted-foreground">{subtitle}</span>}
</div>
</div>

Expand Down Expand Up @@ -117,49 +128,38 @@ const UpdateNoteReceipt = ({ args, result, status }: UpdateNoteReceiptProps) =>
);
};

export const UpdateNoteToolUI = makeAssistantToolUI<UpdateNoteArgs, WorkspaceResult>({
toolName: "updateNote",
render: function UpdateNoteUI({ args, result, status }) {
export const EditItemToolUI = makeAssistantToolUI<EditItemArgs, WorkspaceResult>({
toolName: "editItem",
render: function EditItemUI({ args, result, status }) {
const workspaceId = useWorkspaceStore((state) => state.currentWorkspaceId);

useOptimisticToolUpdate(status, result as any, workspaceId);

// Don't try to parse while still running - wait for completion
let parsed: WorkspaceResult | null = null;
if (status.type === "complete" && result != null) {
try {
parsed = parseWorkspaceResult(result);
} catch (err) {
// Log the error but don't throw - we'll show error state below
console.error("📝 [UpdateNoteTool] Failed to parse result:", err);
console.error("[EditItemTool] Failed to parse result:", err);
parsed = null;
}
}

let content: ReactNode = null;

if (parsed?.success) {
content = <UpdateNoteReceipt args={args} result={parsed as UpdateNoteResult} status={status} />;
content = <EditItemReceipt args={args} result={parsed as EditItemResult} status={status} />;
} else if (status.type === "running") {
content = <ToolUILoadingShell label="Updating note..." />;
const itemName = args?.itemName;
content = <ToolUILoadingShell label={itemName ? `Editing "${itemName}"...` : "Editing..."} />;
} else if (status.type === "complete" && parsed && !parsed.success) {
content = (
<ToolUIErrorShell
label="Failed to update note"
message={parsed.message}
/>
);
content = <ToolUIErrorShell label="Failed to edit" message={parsed.message} />;
} else if (status.type === "incomplete" && status.reason === "error") {
content = (
<ToolUIErrorShell
label="Failed to update note"
message={parsed?.message}
/>
);
content = <ToolUIErrorShell label="Failed to edit" message={parsed?.message} />;
}

return (
<ToolUIErrorBoundary componentName="UpdateNote">
<ToolUIErrorBoundary componentName="EditItem">
{content}
</ToolUIErrorBoundary>
);
Expand Down
Loading