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
7 changes: 0 additions & 7 deletions src/app/(dashboard)/components/question-quiz-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,6 @@ export function QuestionQuizCard({
Po użyciu twojego pierwszego quizu pojawią się tutaj pytania.
</CardDescription>
</CardHeader>
<CardContent className="h-full opacity-0 transition-opacity hover:opacity-100">
<iframe
src="https://pointerpointer.com"
className="h-full w-full rounded-lg"
title="Pointer Pointer Game"
></iframe>
</CardContent>
</Card>
);
}
Expand Down
78 changes: 16 additions & 62 deletions src/app/ai/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { JSONSchema7, LanguageModel, ModelMessage, UIMessage } from "ai";
import { stepCountIs, streamText } from "ai";
import type { JSONSchema7, LanguageModel, UIMessage } from "ai";
import { z } from "zod";

import { env } from "@/env";
import { buildChatModelMessages } from "@/lib/ai/chat-messages";
import type { QuestionContextSnapshot } from "@/lib/ai/chat-messages";
import { resolveImages } from "@/lib/ai/images";
import { getChatModelForUser } from "@/lib/ai/model";
import { isAiModel } from "@/lib/ai/models";
import {
CURRENT_QUESTION_CONTEXT_MARKER,
buildChatSystemPrompt,
buildCurrentQuestionContextPrompt,
collectQuestionImages,
} from "@/lib/ai/prompts";
import { buildChatSystemPrompt } from "@/lib/ai/prompts";
import {
checkRateLimit,
createRateLimitExceededResponse,
Expand Down Expand Up @@ -92,24 +89,6 @@ function truncateQuestionPreview(text: string): string {
return text.length > 180 ? `${text.slice(0, 180)}…` : text;
}

function getModelMessageText(message: ModelMessage): string {
if (typeof message.content === "string") {
return message.content;
}

if (!Array.isArray(message.content)) {
return "";
}

return message.content
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n");
}

function isCurrentQuestionContextMessage(message: ModelMessage): boolean {
return getModelMessageText(message).includes(CURRENT_QUESTION_CONTEXT_MARKER);
}

export async function POST(request: Request) {
if (!env.NEXT_PUBLIC_AI_ENABLED) {
return new Response("AI is not configured", { status: 503 });
Expand Down Expand Up @@ -141,6 +120,7 @@ export async function POST(request: Request) {
userName,
canEdit,
questionContextChange,
questionContextSnapshots,
config,
tools: clientTools,
} = (await request.json()) as {
Expand All @@ -153,6 +133,7 @@ export async function POST(request: Request) {
questionContextChange?: {
previousQuestionOrder?: number | null;
};
questionContextSnapshots?: QuestionContextSnapshot[];
config?: { modelName?: unknown };
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
};
Expand All @@ -170,54 +151,27 @@ export async function POST(request: Request) {
return new Response("AI model is not configured", { status: 503 });
}

const modelMessages = await convertToModelMessages(messages);
const chatQuestion = requestQuestion ?? null;
const chatQuestions = requestQuestions ?? [];
const modelMessages = await buildChatModelMessages({
messages,
chatQuestion,
chatQuestions,
questionContextSnapshots,
legacyQuestionContextChange: questionContextChange,
resolveQuestionImages: resolveImages,
});
const system = buildChatSystemPrompt(
requestQuiz ?? { title: "Quiz", description: "" },
chatQuestions.length,
userName,
canEdit,
);
const imageParts =
chatQuestion === null
? []
: await resolveImages(collectQuestionImages(chatQuestion));

const currentQuestionContext: ModelMessage[] =
chatQuestion === null
? []
: [
{
role: "user" as const,
content:
imageParts.length > 0
? [
{
type: "text" as const,
text: buildCurrentQuestionContextPrompt(chatQuestion, {
questionChanged: questionContextChange !== undefined,
previousQuestionOrder:
questionContextChange?.previousQuestionOrder,
}),
},
...imageParts,
]
: buildCurrentQuestionContextPrompt(chatQuestion, {
questionChanged: questionContextChange !== undefined,
previousQuestionOrder:
questionContextChange?.previousQuestionOrder,
}),
},
];
const messagesWithoutQuestionContext = modelMessages.filter(
(message) => !isCurrentQuestionContextMessage(message),
);

const result = streamText({
model,
system,
messages: [...currentQuestionContext, ...messagesWithoutQuestionContext],
messages: modelMessages,
stopWhen: stepCountIs(5),
tools: {
...frontendTools(clientTools ?? {}),
Expand Down
21 changes: 20 additions & 1 deletion src/app/quiz/[quizId]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,13 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element {
confirmOnboardingAndHide,
cancelOnboarding,
} = useFocusMode(timerStore);
const { isHost: isContinuityHost, peerConnections } = continuity;
const {
disconnect: disconnectContinuity,
isDisconnected: isContinuityDisconnected,
isHost: isContinuityHost,
peerConnections,
reconnect: reconnectContinuity,
} = continuity;
const {
nextAction,
skipQuestion,
Expand Down Expand Up @@ -139,6 +145,16 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element {
});
};

const handleDisconnectContinuity = () => {
disconnectContinuity();
toast.success("Continuity rozłączone w tej karcie");
};

const handleReconnectContinuity = () => {
reconnectContinuity();
toast.success("Continuity włączone dla tego quizu");
};

const handleQuizActivity = (action: () => void) => {
resetInactivityTimer();
action();
Expand Down Expand Up @@ -353,6 +369,8 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element {
isFocusModeActive={isFocusModeActive}
toggleFocusMode={toggleFocusMode}
onToggleHistory={toggleHistory}
isContinuityDisconnected={isContinuityDisconnected}
onReconnectContinuity={handleReconnectContinuity}
isSettingsOpen={isSettingsOpen}
onSettingsOpenChange={setIsSettingsOpen}
/>
Expand Down Expand Up @@ -409,6 +427,7 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element {
<ContinuityDialog
peerConnections={peerConnections}
isContinuityHost={isContinuityHost}
onDisconnectContinuity={handleDisconnectContinuity}
/>

{showAi ? (
Expand Down
68 changes: 65 additions & 3 deletions src/app/search-in-quiz/[quizId]/client.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
"use client";

import { distance } from "fastest-levenshtein";
import { AlertCircleIcon } from "lucide-react";
import { useState } from "react";
import { AlertCircleIcon, PencilLineIcon } from "lucide-react";
import { useContext, useState } from "react";
import type { ReactNode } from "react";

import { AppContext } from "@/app-context";
import { QuickEditQuestionDialog } from "@/components/quiz/quick-edit-question-dialog";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useQuiz } from "@/hooks/use-quiz";

interface SearchInQuizPageClientProps {
Expand Down Expand Up @@ -42,7 +56,11 @@ const highlightMatch = (text: string, q: string): ReactNode => {
};

function SearchInQuizPageContent({ quizId }: { quizId: string }) {
const { user } = useContext(AppContext);
const [query, setQuery] = useState<string>("");
const [editingQuestionId, setEditingQuestionId] = useState<string | null>(
null,
);
const {
data: quiz,
isLoading,
Expand Down Expand Up @@ -153,6 +171,13 @@ function SearchInQuizPageContent({ quizId }: { quizId: string }) {
.toSorted((a, b) => a.relevance - b.relevance);
})();

const canEditQuiz =
quiz != null &&
((quiz.can_edit ?? false) || quiz.creator?.id === user?.user_id);
const editingQuestion =
quiz?.questions.find((question) => question.id === editingQuestionId) ??
null;

if (isLoading) {
return <div className="text-center">Ładowanie...</div>;
}
Expand Down Expand Up @@ -204,6 +229,27 @@ function SearchInQuizPageContent({ quizId }: { quizId: string }) {
/>
)}
</CardTitle>
{canEditQuiz ? (
<CardAction>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon-sm"
onClick={() => {
setEditingQuestionId(q.id);
}}
aria-label="Edytuj lub usuń pytanie"
>
<PencilLineIcon />
</Button>
}
></TooltipTrigger>
<TooltipContent>Edytuj lub usuń pytanie</TooltipContent>
</Tooltip>
</CardAction>
) : null}
</CardHeader>
<CardContent>
<ul className="space-y-1 text-sm">
Expand Down Expand Up @@ -238,6 +284,22 @@ function SearchInQuizPageContent({ quizId }: { quizId: string }) {
<AlertTitle>Brak wyników dla podanego zapytania.</AlertTitle>
</Alert>
)}
{editingQuestion == null ? null : (
<QuickEditQuestionDialog
key={editingQuestion.id}
open={editingQuestionId != null}
onOpenChange={(open) => {
if (!open) {
setEditingQuestionId(null);
}
}}
question={editingQuestion}
quizId={quizId}
onQuestionDeleted={() => {
setEditingQuestionId(null);
}}
/>
)}
</div>
);
}
Expand Down
Loading
Loading