Skip to content

Commit 189b4f3

Browse files
authored
Fix/ai model selector (#33)
* Use any model for AI Agent * restrict embedding model choice to vectors with 1536 dimensions, not 3072. * fix tests * Address type issues etc * Address typing issues etc. add docs, tests and display the models in UI * make error in fetching models explicit * update deps * Fix playwright tests
1 parent f75edd3 commit 189b4f3

43 files changed

Lines changed: 1386 additions & 321 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/landing/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"framer-motion": "^12.34.3",
1818
"geist": "^1.7.0",
1919
"lucide-react": "^0.469.0",
20-
"next": "^15.5.10",
20+
"next": "^15.5.15",
2121
"react": "^19.2.3",
2222
"react-dom": "^19.2.3",
2323
"tailwind-merge": "^2.1.0"

apps/web/e2e/global-teardown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async function globalTeardown() {
4646
args: {
4747
secret: adminSecret,
4848
name: "testing/helpers:cleanupE2ETestData",
49-
mutationArgs: {},
49+
mutationArgsJson: JSON.stringify({}),
5050
},
5151
format: "json",
5252
}),

apps/web/e2e/helpers/test-data.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ async function callInternalMutation<T>(path: string, args: Record<string, unknow
3737
},
3838
body: JSON.stringify({
3939
path: "testAdmin:runTestMutation",
40-
args: { secret: getAdminSecret(), name: path, mutationArgs: args },
40+
args: {
41+
secret: getAdminSecret(),
42+
name: path,
43+
mutationArgsJson: JSON.stringify(args),
44+
},
4145
format: "json",
4246
}),
4347
});

apps/web/e2e/helpers/widget-helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ export async function waitForSurveyVisible(page: Page, timeout = 10000): Promise
315315
export async function submitNPSRating(page: Page, rating: number): Promise<void> {
316316
const frame = getWidgetContainer(page);
317317

318+
await dismissTour(page);
319+
318320
// Click the rating button (0-10)
319321
await frame
320322
.locator(

apps/web/next.config.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,6 @@ const nextConfig = {
4646
// Reduce memory usage during webpack compilation
4747
webpackMemoryOptimizations: true,
4848
},
49-
webpack: (config, { dev }) => {
50-
if (dev) {
51-
// Use filesystem cache to reduce in-memory pressure during dev
52-
config.cache = {
53-
type: "filesystem",
54-
buildDependencies: {
55-
config: [__filename],
56-
},
57-
};
58-
}
59-
return config;
60-
},
6149
async headers() {
6250
return [
6351
{

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"fflate": "^0.8.2",
2323
"lucide-react": "^0.469.0",
2424
"markdown-it": "^14.1.1",
25-
"next": "^15.5.10",
25+
"next": "^15.5.15",
2626
"react": "^19.2.3",
2727
"react-dom": "^19.2.3"
2828
},
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, expect, it, vi } from "vitest";
3+
import type { Id } from "@opencom/convex/dataModel";
4+
import { InboxAiReviewPanel } from "./InboxAiReviewPanel";
5+
import type { InboxAiResponse } from "./inboxRenderTypes";
6+
7+
function messageId(value: string): Id<"messages"> {
8+
return value as Id<"messages">;
9+
}
10+
11+
function responseId(value: string): Id<"aiResponses"> {
12+
return value as Id<"aiResponses">;
13+
}
14+
15+
describe("InboxAiReviewPanel", () => {
16+
it("renders persisted model and provider metadata for AI responses", () => {
17+
const response: InboxAiResponse = {
18+
_id: responseId("response_1"),
19+
createdAt: Date.now(),
20+
query: "How do I reset my password?",
21+
response: "Go to Settings > Security > Reset Password.",
22+
confidence: 0.82,
23+
model: "openai/gpt-5-nano",
24+
provider: "openai",
25+
handedOff: false,
26+
messageId: messageId("message_1"),
27+
sources: [],
28+
deliveredResponseContext: null,
29+
generatedResponseContext: null,
30+
};
31+
32+
render(
33+
<InboxAiReviewPanel
34+
aiResponses={[response]}
35+
orderedAiResponses={[response]}
36+
selectedConversation={null}
37+
onOpenArticle={vi.fn()}
38+
onJumpToMessage={vi.fn()}
39+
getHandoffReasonLabel={(reason) => reason ?? "No reason"}
40+
/>
41+
);
42+
43+
expect(screen.getByText("Model openai/gpt-5-nano")).toBeInTheDocument();
44+
expect(screen.getByText("Provider openai")).toBeInTheDocument();
45+
});
46+
});

apps/web/src/app/inbox/InboxAiReviewPanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ export function InboxAiReviewPanel({
136136
<span className="rounded bg-muted px-2 py-0.5">
137137
{confidenceLabel} {Math.round(confidenceValue * 100)}%
138138
</span>
139+
<span className="rounded bg-muted px-2 py-0.5" data-testid={`inbox-ai-review-model-${response._id}`}>
140+
Model {response.model}
141+
</span>
142+
<span className="rounded bg-muted px-2 py-0.5" data-testid={`inbox-ai-review-provider-${response._id}`}>
143+
Provider {response.provider}
144+
</span>
139145
{response.feedback && (
140146
<span className="rounded bg-muted px-2 py-0.5">
141147
Feedback {response.feedback === "helpful" ? "helpful" : "not helpful"}

apps/web/src/app/inbox/inboxRenderTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface InboxAiResponse {
8787
query: string;
8888
response: string;
8989
confidence: number;
90+
model: string;
91+
provider: string;
9092
handedOff: boolean;
9193
handoffReason?: string | null;
9294
messageId: Id<"messages">;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { act, render, screen, waitFor } from "@testing-library/react";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import type { Id } from "@opencom/convex/dataModel";
4+
import { AIAgentSection } from "./AIAgentSection";
5+
import { useWebAction, useWebMutation, useWebQuery } from "@/lib/convex/hooks";
6+
7+
vi.mock("@/lib/convex/hooks", () => ({
8+
useWebAction: vi.fn(),
9+
useWebMutation: vi.fn(),
10+
useWebQuery: vi.fn(),
11+
webActionRef: vi.fn((functionName: string) => functionName),
12+
webMutationRef: vi.fn((functionName: string) => functionName),
13+
webQueryRef: vi.fn((functionName: string) => functionName),
14+
}));
15+
16+
describe("AIAgentSection model discovery fallbacks", () => {
17+
const workspaceId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as unknown as Id<"workspaces">;
18+
const aiSettingsFixture = {
19+
enabled: true,
20+
model: "openai/gpt-5-nano",
21+
confidenceThreshold: 0.6,
22+
knowledgeSources: ["articles"],
23+
personality: "",
24+
handoffMessage: "",
25+
suggestionsEnabled: false,
26+
embeddingModel: "text-embedding-3-small",
27+
lastConfigError: null,
28+
} as const;
29+
30+
let listAvailableModelsMock: ReturnType<typeof vi.fn>;
31+
let rejectDiscovery: ((reason?: unknown) => void) | undefined;
32+
33+
beforeEach(() => {
34+
vi.clearAllMocks();
35+
vi.spyOn(console, "error").mockImplementation(() => {});
36+
37+
const mockedUseWebQuery = useWebQuery as unknown as ReturnType<typeof vi.fn>;
38+
mockedUseWebQuery.mockImplementation((_: unknown, args: unknown) => {
39+
if (args === "skip") {
40+
return undefined;
41+
}
42+
43+
return aiSettingsFixture;
44+
});
45+
46+
listAvailableModelsMock = vi.fn(
47+
() =>
48+
new Promise((_, reject) => {
49+
rejectDiscovery = reject;
50+
})
51+
);
52+
53+
const mockedUseWebAction = useWebAction as unknown as ReturnType<typeof vi.fn>;
54+
mockedUseWebAction.mockReturnValue(listAvailableModelsMock);
55+
56+
const mockedUseWebMutation = useWebMutation as unknown as ReturnType<typeof vi.fn>;
57+
mockedUseWebMutation.mockReturnValue(vi.fn().mockResolvedValue(undefined));
58+
});
59+
60+
it("stops showing the loading placeholder when model discovery fails", async () => {
61+
render(<AIAgentSection workspaceId={workspaceId} />);
62+
63+
await waitFor(() => {
64+
expect(listAvailableModelsMock).toHaveBeenCalledWith({
65+
workspaceId,
66+
selectedModel: aiSettingsFixture.model,
67+
});
68+
});
69+
70+
expect(screen.getByRole("option", { name: /loading discovered models/i })).toBeInTheDocument();
71+
72+
await act(async () => {
73+
rejectDiscovery?.(new Error("Discovery failed"));
74+
});
75+
76+
await waitFor(() => {
77+
expect(screen.getByRole("option", { name: /model discovery unavailable/i })).toBeInTheDocument();
78+
});
79+
80+
expect(
81+
screen.getByText(/model discovery is currently unavailable\. enter a model id manually/i)
82+
).toBeInTheDocument();
83+
expect(
84+
screen.queryByRole("option", { name: /loading discovered models/i })
85+
).not.toBeInTheDocument();
86+
});
87+
});

0 commit comments

Comments
 (0)