From e3ec54ecc21e695cec55945012c52030289fc5b6 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Sun, 7 Sep 2025 21:42:21 -0400 Subject: [PATCH 1/7] Add dedent package --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index faf9e73..343217b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@langchain/community": "^0.3.53", "@langchain/core": "^0.3.72", "ai": "^4.3.19", + "dedent": "^1.7.0", "react-basic-contenteditable": "^1.0.6", "react-markdown": "^10.1.0" }, @@ -6972,6 +6973,20 @@ "node": ">=0.10" } }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", diff --git a/package.json b/package.json index 1c37de1..0aef9d5 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@langchain/community": "^0.3.53", "@langchain/core": "^0.3.72", "ai": "^4.3.19", + "dedent": "^1.7.0", "react-basic-contenteditable": "^1.0.6", "react-markdown": "^10.1.0" }, From 2ba07923978180071382898470e09c7497ed7b23 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Sun, 7 Sep 2025 21:49:01 -0400 Subject: [PATCH 2/7] Update base provider to set system prompt --- src/plugin/Panel/index.tsx | 16 ++------ src/plugin/base_provider.tsx | 73 ++++++++++++++++++++++++++++++------ src/plugin/context/index.ts | 10 +++-- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/plugin/Panel/index.tsx b/src/plugin/Panel/index.tsx index 4d9517a..6182baa 100644 --- a/src/plugin/Panel/index.tsx +++ b/src/plugin/Panel/index.tsx @@ -22,15 +22,10 @@ export function PluginPanelComponent(props: CloverPlugin & PluginProps) { useEffect(() => { if (provider) { provider.update_dispatch(dispatch); + provider.update_plugin_state(state); + provider.set_system_prompt(); dispatch({ type: "updateProvider", provider }); } - - if (!state.systemPrompt) { - dispatch({ - type: "setSystemPrompt", - systemPrompt: `You are a helpful assistant that can answer questions about the item in the viewer`, - }); - } }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -51,16 +46,11 @@ export function PluginPanelComponent(props: CloverPlugin & PluginProps) { useEffect(() => { if (state.manifest) { - // Update system prompt with manifest metadata - dispatch({ - type: "setSystemPrompt", - systemPrompt: `You are a helpful assistant that can answer questions about the item in the viewer. Here is the manifest data for the item:\n\n${JSON.stringify(state.manifest["metadata"], null, 2)}`, - }); const label = state.manifest?.label ?? undefined; const title = getLabelByUserLanguage(label); setItemTitle(title.length > 0 ? title[0] : "this item"); } - }, [state.manifest, dispatch]); + }, [state.manifest]); useEffect(() => { dispatch({ diff --git a/src/plugin/base_provider.tsx b/src/plugin/base_provider.tsx index 47371ef..047cc6b 100644 --- a/src/plugin/base_provider.tsx +++ b/src/plugin/base_provider.tsx @@ -1,5 +1,8 @@ -import type { PluginContextActions } from "@context"; +import type { PluginContextActions, PluginContextStore } from "@context"; +import { ManifestNormalized } from "@iiif/presentation-3-normalized"; import type { ConversationState, Message } from "@types"; +import { getLabelByUserLanguage } from "@utils"; +import dedent from "dedent"; import type { Dispatch } from "react"; type ProviderStatus = "initializing" | "ready" | "error"; @@ -9,42 +12,72 @@ type ProviderStatus = "initializing" | "ready" | "error"; * */ export abstract class BaseProvider { - #dispatch: Dispatch | undefined; + #plugin_dispatch: Dispatch | undefined; + #plugin_state: PluginContextStore | undefined; #status: ProviderStatus; constructor() { this.#status = "ready"; } - private get dispatch(): Dispatch { - if (!this.#dispatch) { + private get plugin_dispatch(): Dispatch { + if (!this.#plugin_dispatch) { throw new Error("Provider dispatch not initialized."); } - return this.#dispatch; + return this.#plugin_dispatch; } /** * Sets the dispatch function to allow the provider to update Plugin state */ - private set dispatch(dispatch: Dispatch) { - this.#dispatch = dispatch; + private set plugin_dispatch(dispatch: Dispatch) { + this.#plugin_dispatch = dispatch; + } + + private get plugin_state(): PluginContextStore { + if (!this.#plugin_state) { + throw new Error("Provider plugin_state not initialized."); + } + return this.#plugin_state; + } + + private set plugin_state(state: PluginContextStore) { + this.#plugin_state = state; } /** * Add messages to the Plugin state */ protected add_messages(messages: Message[]) { - this.dispatch({ + this.plugin_dispatch({ type: "addMessages", messages, }); } + /** + * Generate a system prompt based on the provided manifest + * + * @param manifest the IIIF manifest + * @returns a system prompt string based on the manifest data + */ + protected generate_system_prompt(manifest: ManifestNormalized) { + return dedent` + You are a helpful assistant that can answer questions about the item in the image viewer. + + Here is the manifest data for the item: + + ## Title: ${getLabelByUserLanguage(manifest.label ?? undefined)[0] ?? "N/A"} + ## Summary: ${manifest.summary ? getLabelByUserLanguage(manifest.summary)[0] : "N/A"} + ## Raw Metadata: ${JSON.stringify(manifest.metadata)} + `; + } + /** * Update the Plugin's conversation state. */ protected set_conversation_state(state: ConversationState) { - this.dispatch({ + this.plugin_dispatch({ type: "setConversationState", conversationState: state, }); @@ -54,7 +87,7 @@ export abstract class BaseProvider { * Update the last message in the Plugin state. */ protected update_last_message(message: Message) { - this.dispatch({ + this.plugin_dispatch({ type: "updateLastMessage", message, }); @@ -64,7 +97,7 @@ export abstract class BaseProvider { * Update the Plugin state with the current provider. */ protected update_plugin_provider() { - this.dispatch({ + this.plugin_dispatch({ type: "updateProvider", provider: this, }); @@ -83,6 +116,18 @@ export abstract class BaseProvider { this.#status = value; } + /** + * Set the system prompt in the Plugin state based on the current manifest. + */ + set_system_prompt() { + const systemPrompt = this.generate_system_prompt(this.plugin_state.manifest); + + this.plugin_dispatch({ + type: "setSystemPrompt", + systemPrompt, + }); + } + /** * A component that providers can implement to set up their UI. */ @@ -91,6 +136,10 @@ export abstract class BaseProvider { } update_dispatch(dispatch: Dispatch) { - this.dispatch = dispatch; + this.plugin_dispatch = dispatch; + } + + update_plugin_state(context: PluginContextStore) { + this.plugin_state = context; } } diff --git a/src/plugin/context/index.ts b/src/plugin/context/index.ts index 8561ab5..4f86a2c 100644 --- a/src/plugin/context/index.ts +++ b/src/plugin/context/index.ts @@ -1,6 +1,8 @@ // using a barrel file helps tsc-alias resolve the path correctly -import type { PluginContextActions } from "./plugin-context"; -import { PluginContextProvider, usePlugin } from "./plugin-context"; -export { PluginContextProvider, usePlugin }; -export type { PluginContextActions }; +export { + PluginContextProvider, + usePlugin, + type PluginContextActions, + type PluginContextStore, +} from "./plugin-context"; From 286a757f63aa28c8f28570476dfa54fc110b9f5e Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Sun, 7 Sep 2025 21:50:18 -0400 Subject: [PATCH 3/7] Fix types error --- src/providers/userTokenProvider/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/providers/userTokenProvider/index.tsx b/src/providers/userTokenProvider/index.tsx index bb04002..0fbe3a8 100644 --- a/src/providers/userTokenProvider/index.tsx +++ b/src/providers/userTokenProvider/index.tsx @@ -4,7 +4,7 @@ import { createOpenAI, type OpenAIProvider } from "@ai-sdk/openai"; import { Button, Heading, Input } from "@components"; import { Tool } from "@langchain/core/tools"; import type { AssistantMessage, Message } from "@types"; -import { streamText, tool } from "ai"; +import { CoreMessage, streamText, tool } from "ai"; import React from "react"; import { BaseProvider } from "../../plugin/base_provider"; import { ModelSelection } from "./components/ModelSelection"; @@ -45,7 +45,7 @@ export class UserTokenProvider extends BaseProvider { * @param message * @returns a formatted message */ - #format_message(message: Message) { + #format_message(message: Message): CoreMessage { switch (message.role) { case "user": return { @@ -181,7 +181,6 @@ export class UserTokenProvider extends BaseProvider { model, tools: this.#transform_tools(), maxSteps: this.max_steps, - // @ts-expect-error - there is a type mismatch here, but it works messages: all_messages.map(this.#format_message), }); From ba512ccb51d16a90011ad73e2ea12f3730b008af Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Sun, 7 Sep 2025 22:00:32 -0400 Subject: [PATCH 4/7] Add canvas context to user messages --- src/plugin/Panel/ChatInput/index.tsx | 37 +++++++++++++++++++++++ src/providers/userTokenProvider/index.tsx | 16 +++++++++- src/types.d.ts | 8 +++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/plugin/Panel/ChatInput/index.tsx b/src/plugin/Panel/ChatInput/index.tsx index afbe03b..3e34e88 100644 --- a/src/plugin/Panel/ChatInput/index.tsx +++ b/src/plugin/Panel/ChatInput/index.tsx @@ -1,7 +1,10 @@ import { Button, Textarea } from "@components"; import { usePlugin } from "@context"; import { Add, ArrowUp, Clear } from "@icons"; +import { Traverse, serializeConfigPresentation3 } from "@iiif/parser"; +import { Canvas } from "@iiif/presentation-3"; import { MediaContent, UserMessage } from "@types"; +import { getLabelByUserLanguage } from "@utils"; import type { FC } from "react"; import { useState } from "react"; import { SelectedMedia } from "./SelectedMedia"; @@ -30,6 +33,33 @@ export const ChatInput: FC = () => { setTextareaError(""); setFormState("loading"); + const canvas = state.vault.serialize( + { + type: "Canvas", + id: state.activeCanvas.id, + }, + serializeConfigPresentation3, + ); + + const annotationTexts: string[] = []; + const traverse = new Traverse({ + annotation: [ + (a) => { + if ( + a.body && + typeof a.body === "object" && + "type" in a.body && + a.body.type === "TextualBody" && + a.body.value + ) { + annotationTexts.push(a.body.value); + } + }, + ], + }); + + traverse.traverseCanvas(canvas); + const userMessage: UserMessage = { role: "user", content: [ @@ -38,6 +68,13 @@ export const ChatInput: FC = () => { content: input, }, ], + context: { + canvas: { + annotations: annotationTexts, + id: state.activeCanvas.id, + label: getLabelByUserLanguage(state.activeCanvas.label ?? undefined)[0], + }, + }, }; if (state.selectedMedia.length) { diff --git a/src/providers/userTokenProvider/index.tsx b/src/providers/userTokenProvider/index.tsx index 0fbe3a8..c24daa6 100644 --- a/src/providers/userTokenProvider/index.tsx +++ b/src/providers/userTokenProvider/index.tsx @@ -5,6 +5,7 @@ import { Button, Heading, Input } from "@components"; import { Tool } from "@langchain/core/tools"; import type { AssistantMessage, Message } from "@types"; import { CoreMessage, streamText, tool } from "ai"; +import dedent from "dedent"; import React from "react"; import { BaseProvider } from "../../plugin/base_provider"; import { ModelSelection } from "./components/ModelSelection"; @@ -54,7 +55,20 @@ export class UserTokenProvider extends BaseProvider { if (c.type === "media") { return { type: "image", image: c.content.src }; } - return { type: "text", text: c.content }; + + const canvas = message.context.canvas; + // prettier-ignore + const context = dedent.withOptions({ alignValues: true })` + ## Context + The following context is about the current canvas in the image viewer. + Use this information if possible to inform your answer + + ### Canvas${canvas.label ? ` + - Label: ${canvas.label}` : ""}${canvas.annotations.length ? ` + - Annotations: ${canvas.annotations.join(", ")}` : ""} + `; + + return { type: "text", text: c.content + "\n" + context }; }), }; case "assistant": diff --git a/src/types.d.ts b/src/types.d.ts index 53002de..6cb1412 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -39,6 +39,14 @@ export type AssistantMessage = { export interface UserMessage { content: (TextContent | MediaContent)[]; + /** Context that can be added to user messages when generating a response */ + context: { + canvas: { + annotations: string[]; + id: string; + label: string | undefined; + }; + }; role: Extract; } From 09d4233daeeb3f45c840d38038b1d3021257b2b1 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Mon, 8 Sep 2025 11:02:04 -0400 Subject: [PATCH 5/7] Refactor to use normalized canvas --- src/plugin/Panel/ChatInput/index.tsx | 36 +----------------- src/plugin/base_provider.tsx | 4 +- src/providers/userTokenProvider/index.tsx | 45 ++++++++++++++++++++--- src/types.d.ts | 7 +--- 4 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/plugin/Panel/ChatInput/index.tsx b/src/plugin/Panel/ChatInput/index.tsx index 3e34e88..297e072 100644 --- a/src/plugin/Panel/ChatInput/index.tsx +++ b/src/plugin/Panel/ChatInput/index.tsx @@ -1,10 +1,7 @@ import { Button, Textarea } from "@components"; import { usePlugin } from "@context"; import { Add, ArrowUp, Clear } from "@icons"; -import { Traverse, serializeConfigPresentation3 } from "@iiif/parser"; -import { Canvas } from "@iiif/presentation-3"; import { MediaContent, UserMessage } from "@types"; -import { getLabelByUserLanguage } from "@utils"; import type { FC } from "react"; import { useState } from "react"; import { SelectedMedia } from "./SelectedMedia"; @@ -33,33 +30,6 @@ export const ChatInput: FC = () => { setTextareaError(""); setFormState("loading"); - const canvas = state.vault.serialize( - { - type: "Canvas", - id: state.activeCanvas.id, - }, - serializeConfigPresentation3, - ); - - const annotationTexts: string[] = []; - const traverse = new Traverse({ - annotation: [ - (a) => { - if ( - a.body && - typeof a.body === "object" && - "type" in a.body && - a.body.type === "TextualBody" && - a.body.value - ) { - annotationTexts.push(a.body.value); - } - }, - ], - }); - - traverse.traverseCanvas(canvas); - const userMessage: UserMessage = { role: "user", content: [ @@ -69,11 +39,7 @@ export const ChatInput: FC = () => { }, ], context: { - canvas: { - annotations: annotationTexts, - id: state.activeCanvas.id, - label: getLabelByUserLanguage(state.activeCanvas.label ?? undefined)[0], - }, + canvas: state.activeCanvas, }, }; diff --git a/src/plugin/base_provider.tsx b/src/plugin/base_provider.tsx index 047cc6b..0dcc268 100644 --- a/src/plugin/base_provider.tsx +++ b/src/plugin/base_provider.tsx @@ -34,14 +34,14 @@ export abstract class BaseProvider { this.#plugin_dispatch = dispatch; } - private get plugin_state(): PluginContextStore { + protected get plugin_state(): PluginContextStore { if (!this.#plugin_state) { throw new Error("Provider plugin_state not initialized."); } return this.#plugin_state; } - private set plugin_state(state: PluginContextStore) { + protected set plugin_state(state: PluginContextStore) { this.#plugin_state = state; } diff --git a/src/providers/userTokenProvider/index.tsx b/src/providers/userTokenProvider/index.tsx index c24daa6..bce2511 100644 --- a/src/providers/userTokenProvider/index.tsx +++ b/src/providers/userTokenProvider/index.tsx @@ -2,6 +2,8 @@ import { createAnthropic, type AnthropicProvider } from "@ai-sdk/anthropic"; import { createGoogleGenerativeAI, type google } from "@ai-sdk/google"; import { createOpenAI, type OpenAIProvider } from "@ai-sdk/openai"; import { Button, Heading, Input } from "@components"; +import { serializeConfigPresentation3, Traverse } from "@iiif/parser"; +import type { Canvas } from "@iiif/presentation-3"; import { Tool } from "@langchain/core/tools"; import type { AssistantMessage, Message } from "@types"; import { CoreMessage, streamText, tool } from "ai"; @@ -45,8 +47,13 @@ export class UserTokenProvider extends BaseProvider { * * @param message * @returns a formatted message + * + * @privateRemarks + * + * Use an arrow function so `this` references the `UserTokenProvider` class */ - #format_message(message: Message): CoreMessage { + #format_message = (message: Message): CoreMessage => { + debugger; switch (message.role) { case "user": return { @@ -56,7 +63,33 @@ export class UserTokenProvider extends BaseProvider { return { type: "image", image: c.content.src }; } - const canvas = message.context.canvas; + const canvas = this.plugin_state.vault.serialize( + { + type: "Canvas", + id: message.context.canvas.id, + }, + serializeConfigPresentation3, + ); + + const annotationTexts: string[] = []; + const traverse = new Traverse({ + annotation: [ + (a) => { + if ( + a.body && + typeof a.body === "object" && + "type" in a.body && + a.body.type === "TextualBody" && + a.body.value + ) { + annotationTexts.push(a.body.value); + } + }, + ], + }); + + traverse.traverseCanvas(canvas); + // prettier-ignore const context = dedent.withOptions({ alignValues: true })` ## Context @@ -64,8 +97,8 @@ export class UserTokenProvider extends BaseProvider { Use this information if possible to inform your answer ### Canvas${canvas.label ? ` - - Label: ${canvas.label}` : ""}${canvas.annotations.length ? ` - - Annotations: ${canvas.annotations.join(", ")}` : ""} + - Label: ${canvas.label}` : ""}${annotationTexts.length ? ` + - Annotations: ${annotationTexts.join(", ")}` : ""} `; return { type: "text", text: c.content + "\n" + context }; @@ -79,7 +112,7 @@ export class UserTokenProvider extends BaseProvider { // @ts-expect-error - this is a catch-all for unsupported roles throw new Error(`Unsupported message role: ${message.role}`); } - } + }; #is_valid_model_provider_model(provider: Provider, model: string): boolean { return this.models_by_provider[provider].includes(model); @@ -191,6 +224,8 @@ export class UserTokenProvider extends BaseProvider { try { const model = this.setup_model(this.selected_provider, this.user_token, this.selected_model); + debugger; + const { fullStream } = streamText({ model, tools: this.#transform_tools(), diff --git a/src/types.d.ts b/src/types.d.ts index 6cb1412..24cc98f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,3 +1,4 @@ +import type { CanvasNormalized } from "@iiif/presentation-3-normalized"; export type ConversationState = "idle" | "assistant_responding" | "error"; export type Role = "assistant" | "system" | "user"; @@ -41,11 +42,7 @@ export interface UserMessage { content: (TextContent | MediaContent)[]; /** Context that can be added to user messages when generating a response */ context: { - canvas: { - annotations: string[]; - id: string; - label: string | undefined; - }; + canvas: CanvasNormalized; }; role: Extract; } From d588255498cd5e8ed1edbf39269069ed35811361 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Mon, 8 Sep 2025 15:02:10 -0400 Subject: [PATCH 6/7] Update how messages are transformed with context --- src/providers/userTokenProvider/index.tsx | 92 ++++++++++++----------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/src/providers/userTokenProvider/index.tsx b/src/providers/userTokenProvider/index.tsx index bce2511..73b5a13 100644 --- a/src/providers/userTokenProvider/index.tsx +++ b/src/providers/userTokenProvider/index.tsx @@ -6,6 +6,7 @@ import { serializeConfigPresentation3, Traverse } from "@iiif/parser"; import type { Canvas } from "@iiif/presentation-3"; import { Tool } from "@langchain/core/tools"; import type { AssistantMessage, Message } from "@types"; +import { getLabelByUserLanguage } from "@utils"; import { CoreMessage, streamText, tool } from "ai"; import dedent from "dedent"; import React from "react"; @@ -52,8 +53,7 @@ export class UserTokenProvider extends BaseProvider { * * Use an arrow function so `this` references the `UserTokenProvider` class */ - #format_message = (message: Message): CoreMessage => { - debugger; + #format_message = (message: Message, index: number, messages: Message[]): CoreMessage => { switch (message.role) { case "user": return { @@ -63,45 +63,55 @@ export class UserTokenProvider extends BaseProvider { return { type: "image", image: c.content.src }; } - const canvas = this.plugin_state.vault.serialize( - { - type: "Canvas", - id: message.context.canvas.id, - }, - serializeConfigPresentation3, - ); - - const annotationTexts: string[] = []; - const traverse = new Traverse({ - annotation: [ - (a) => { - if ( - a.body && - typeof a.body === "object" && - "type" in a.body && - a.body.type === "TextualBody" && - a.body.value - ) { - annotationTexts.push(a.body.value); - } + const prevMessages = messages.slice(0, index); + const lastUserMessage = prevMessages.findLast((m) => m.role === "user"); + let context = ""; + + // only add new context to the messages when it changes to save on tokens + if ( + !lastUserMessage || + lastUserMessage.context.canvas.id !== message.context.canvas.id + ) { + const canvas = this.plugin_state.vault.serialize( + { + type: "Canvas", + id: message.context.canvas.id, }, - ], - }); - - traverse.traverseCanvas(canvas); - - // prettier-ignore - const context = dedent.withOptions({ alignValues: true })` - ## Context - The following context is about the current canvas in the image viewer. - Use this information if possible to inform your answer - - ### Canvas${canvas.label ? ` - - Label: ${canvas.label}` : ""}${annotationTexts.length ? ` - - Annotations: ${annotationTexts.join(", ")}` : ""} - `; - - return { type: "text", text: c.content + "\n" + context }; + serializeConfigPresentation3, + ); + + const annotationTexts: string[] = []; + const traverse = new Traverse({ + annotation: [ + (a) => { + if ( + a.body && + typeof a.body === "object" && + "type" in a.body && + a.body.type === "TextualBody" && + a.body.value + ) { + annotationTexts.push(a.body.value); + } + }, + ], + }); + + traverse.traverseCanvas(canvas); + + // prettier-ignore + context = dedent.withOptions({ alignValues: true })` + ## Context + The following context is about the latest Canvas in the image viewer. + Use this information if possible to inform your answer. + + ### Canvas${canvas.label ? ` + - Label: ${getLabelByUserLanguage(canvas.label)[0]}` : ""}${annotationTexts.length ? ` + - Annotations: ${annotationTexts.join(", ")}` : ""} + `; + } + + return { type: "text", text: c.content + `${context ? "\n" + context : ""}` }; }), }; case "assistant": @@ -224,8 +234,6 @@ export class UserTokenProvider extends BaseProvider { try { const model = this.setup_model(this.selected_provider, this.user_token, this.selected_model); - debugger; - const { fullStream } = streamText({ model, tools: this.#transform_tools(), From 08fca7f46e4bf8f9bc39078b694a0fa0174f01e2 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Mon, 8 Sep 2025 15:42:06 -0400 Subject: [PATCH 7/7] Fix potential array access error --- src/plugin/base_provider.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugin/base_provider.tsx b/src/plugin/base_provider.tsx index 0dcc268..8658953 100644 --- a/src/plugin/base_provider.tsx +++ b/src/plugin/base_provider.tsx @@ -62,13 +62,15 @@ export abstract class BaseProvider { * @returns a system prompt string based on the manifest data */ protected generate_system_prompt(manifest: ManifestNormalized) { + const title = getLabelByUserLanguage(manifest.label ?? undefined)?.[0] ?? "N/A"; + const summary = getLabelByUserLanguage(manifest.summary ?? undefined)?.[0] ?? "N/A"; return dedent` You are a helpful assistant that can answer questions about the item in the image viewer. Here is the manifest data for the item: - ## Title: ${getLabelByUserLanguage(manifest.label ?? undefined)[0] ?? "N/A"} - ## Summary: ${manifest.summary ? getLabelByUserLanguage(manifest.summary)[0] : "N/A"} + ## Title: ${title} + ## Summary: ${summary} ## Raw Metadata: ${JSON.stringify(manifest.metadata)} `; }