From 39acf8be3a3c245b6f525360ccc8673da632f960 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 12 Feb 2025 13:54:42 -0500
Subject: chat box fits width by default

---
 src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx')

diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index f8fe531ab..16da360fc 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -1046,5 +1046,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
  */
 Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, {
     layout: { view: ChatBox, dataField: 'data' },
-    options: { acl: '', chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
+    options: { acl: '', _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
 });
-- 
cgit v1.2.3-70-g09d2


From f6581aebd603dcdd84e033acffe46352ce5a4485 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 19 Feb 2025 15:05:47 -0500
Subject: more gptpopup cleanup.

---
 src/client/apis/gpt/GPT.ts                         | 59 +++++++-------
 src/client/util/CurrentUserUtils.ts                |  6 +-
 src/client/util/Import & Export/ImageUtils.ts      | 12 +--
 src/client/views/StyleProviderQuiz.tsx             |  2 +-
 src/client/views/collections/CollectionSubView.tsx | 33 ++++----
 src/client/views/global/globalScripts.ts           | 14 ++--
 src/client/views/nodes/ComparisonBox.tsx           |  6 +-
 .../views/nodes/chatbot/agentsystem/Agent.ts       |  3 +-
 .../nodes/chatbot/chatboxcomponents/ChatBox.tsx    |  3 +-
 .../views/nodes/chatbot/tools/ImageCreationTool.ts | 36 ++++----
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 95 ++++++++++------------
 src/fields/Doc.ts                                  |  2 +-
 12 files changed, 126 insertions(+), 145 deletions(-)

(limited to 'src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx')

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index e1ae99d97..6d9bc1d06 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,12 +1,11 @@
 import { ChatCompletionMessageParam, Image } from 'openai/resources';
 import { openai } from './setup';
 
-export enum GPTTypeStyle {
+export enum GPTDocCommand {
     AssignTags = 1,
     Filter = 2,
-    DocInfo = 3,
-    GeneralInfo = 4,
-    SortDocs = 5,
+    GetInfo = 3,
+    Sort = 4,
 }
 
 export const DescriptionSeperator = '======';
@@ -18,8 +17,6 @@ enum GPTCallType {
     EDIT = 'edit',
     CHATCARD = 'chatcard', // a single flashcard style response to a question
     FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic
-    QUIZ = 'quiz',
-    SORT = 'sort',
     DESCRIBE = 'describe',
     MERMAID = 'mermaid',
     DATA = 'data',
@@ -27,15 +24,17 @@ enum GPTCallType {
     PRONUNCIATION = 'pronunciation',
     DRAW = 'draw',
     COLOR = 'color',
-    RUBRIC = 'rubric', // needs to be filled in below
-    TYPE = 'type', // needs to be filled in below
-    SUBSET = 'subset', // needs to be filled in below
-    INFO = 'info', // needs to be filled in below
     TEMPLATE = 'template',
     VIZSUM = 'vizsum',
     VIZSUM2 = 'vizsum2',
     FILL = 'fill',
     COMPLETEPROMPT = 'completeprompt',
+    QUIZDOC = 'quiz_doc',
+    MAKERUBRIC = 'make_rubric', // create a definition rubric for a document to be used when quizzing the user
+    COMMANDTYPE = 'command_type', // Determine the type of command being made (GPTQueryType - eg., AssignTags, Sort, Filter, DocInfo, GenInfo) and possibly some parameters (eg, Tag type for Tags)
+    SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions
+    DOCINFO = 'doc_info', // provide information about a document
+    SORTDOCS = 'sort_docs',
 }
 
 type GPTCallOpts = {
@@ -69,7 +68,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         temp: 0.5,
         prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ",
     },
-    sort: {
+    sort_docs: {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
@@ -89,7 +88,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Create a title for each question and asnwer that is labeled as "title".  Do not label each flashcard and do not include asterisks: ',
     },
     chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
-    quiz: {
+    quiz_doc: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -138,20 +137,20 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         temp: 0.5,
         prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
     },
-    type: {
+    command_type: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
         prompt: `I'm going to provide you with a question. 
                  Based on the question, is the user asking you to 
-                     ${GPTTypeStyle.AssignTags}. Assigns docs with tags(like star / heart etc)/labels, 
-                     ${GPTTypeStyle.DocInfo}. Provide information about a specific doc 
-                     ${GPTTypeStyle.Filter}. Filter docs based on a question/information 
-                     ${GPTTypeStyle.GeneralInfo}. Provide general information 
-                     ${GPTTypeStyle.SortDocs}. Put cards in a specific order. 
-                Answer with only the number for 2-5. For number one, provide the number (1) and the appropriate tag`,
+                     ${GPTDocCommand.AssignTags}. Assigns docs with tags(like star / heart etc)/labels.
+                     ${GPTDocCommand.GetInfo}. Provide information about a specific doc.
+                     ${GPTDocCommand.Filter}. Filter docs based on a question/information.
+                     ${GPTDocCommand.Sort}. Put docs in a specific order. 
+                Answer with only the number for ${GPTDocCommand.GetInfo}-${GPTDocCommand.Sort}. 
+                For number one, provide the number (${GPTDocCommand.AssignTags}) and the appropriate tag`,
     },
-    subset: {
+    subset_docs: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -164,14 +163,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
                 It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
     },
 
-    info: {
+    doc_info: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
         prompt: `Answer the user's question with a short (<100 word) response. 
                  If a particular document is selected I will provide that information (which may help with your response)`,
     },
-    rubric: {
+    make_rubric: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -188,17 +187,15 @@ let lastResp = '';
  * @returns AI Output
  */
 const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
-    const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? '.' : '');
+    const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZDOC, GPTCallType.STACK].includes(callType) ? '.' : '');
     const opts = callTypeMap[callType];
     if (!opts) {
         console.log('The query type:' + callType + ' requires a configuration.');
         return 'Error connecting with API.';
     }
-    if (lastCall === inputText && dontCache !== true) return lastResp;
+    if (lastCall === inputText && dontCache !== true && lastResp) return lastResp;
     try {
-        lastCall = inputText;
-
-        const usePrompt = prompt ? prompt + opts.prompt : opts.prompt;
+        const usePrompt = prompt ? prompt + '.' + opts.prompt : opts.prompt;
         const messages: ChatCompletionMessageParam[] = [
             { role: 'system', content: usePrompt },
             { role: 'user', content: inputText },
@@ -210,8 +207,12 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: s
             temperature: opts.temp,
             max_tokens: opts.maxTokens,
         });
-        lastResp = response.choices[0].message.content ?? '';
-        return lastResp;
+        const result = response.choices[0].message.content ?? '';
+        if (!dontCache) {
+            lastResp = result;
+            lastCall = inputText;
+        }
+        return result;
     } catch (err) {
         console.log(err);
         return 'Error connecting with API.';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 6ca181d92..fb349abd9 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -710,11 +710,7 @@ pie title Minerals in my tap water
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Color",     icon:"palette",        toolTip:"Sort by document color",                         btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color",      funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Tags",      icon:"bolt",           toolTip:"Sort by document's tags",                        btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag",        funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
-            { title: "Sort",      icon: "sort" ,         toolTip: "Manage sort order / lock status",               btnType: ButtonType.MultiToggleButton,               toolType:"alignment", ignoreClick: true,
-              subMenu: [
-                { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up",  toolType:"up",  ignoreClick: true,  scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
-                { title: "Descending",toolTip: "Sort the cards in descending order",btnType: ButtonType.ToggleButton, icon: "sort-down",toolType:"down",ignoreClick: true,  scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
-            ]},
+            { title: "Reverse",   icon: "sort-up",       toolTip: "Sort the cards in reverse order",               btnType: ButtonType.ToggleButton, expertMode: false, toolType:"reverse",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
          ]
     }
 
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 8d4eefa7e..f73149fdc 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -4,22 +4,16 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
+import { Upload } from '../../../server/SharedMediaTypes';
 import { Networking } from '../../Network';
 
 export namespace ImageUtils {
-    export type imgInfo = {
-        contentSize: number;
-        nativeWidth: number;
-        nativeHeight: number;
-        source: string;
-        exifData: { error: string | undefined; data: string };
-    };
-    export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => {
+    export const ExtractImgInfo = async (document: Doc): Promise<Upload.InspectionResults | undefined> => {
         const field = Cast(document.data, ImageField);
         return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
     };
 
-    export const AssignImgInfo = (document: Doc, data?: imgInfo) => {
+    export const AssignImgInfo = (document: Doc, data?: Upload.InspectionResults) => {
         if (data) {
             data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth);
             const proto = document[DocData];
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index b3fb8c930..d8eeb3490 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -265,7 +265,7 @@ export namespace styleProviderQuiz {
                     '. ' +
                     rubricText +
                     '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
-                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
+                const response = await gptAPICall(queryText, GPTCallType.QUIZDOC);
                 const hexSent = extractHexAndSentences(response);
                 doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                 doc.backgroundColor = '#' + hexSent.hexNumber;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 954f9aa4c..b40cd2761 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -37,6 +37,9 @@ export enum docSortings {
     Tag = 'tag',
     None = '',
 }
+
+export const ChatSortField = 'chat_sortIndex';
+
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
     isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -229,23 +232,21 @@ export function CollectionSubView<X>() {
 
         childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
             const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
-            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']);
             const docs = docsIn.slice();
-            if (sortType) {
-                docs.sort((docA, docB) => {
-                    const [typeA, typeB] = (() => {
-                        switch (sortType) {
-                            default:                
-                            case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
-                            case docSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
-                            case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
-                            case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
-                            case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
-                        }
-                    })(); //prettier-ignore
-                    return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
-                });
-            }
+            sortType && docs.sort((docA, docB) => {
+                const [typeA, typeB] = (() => {
+                    switch (sortType) {
+                        default:                
+                        case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                        case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999),                   NumCast(docB[ChatSortField], 9999)];
+                        case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                        case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+                        case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
+                    }
+                })(); 
+                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1);
+            }); //prettier-ignore
             if (dragIndex !== -1) {
                 const draggedDoc = DragManager.docsBeingDragged[0];
                 const originalIndex = docs.findIndex(doc => doc === draggedDoc);
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2bc0e3338..79873ed8f 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -152,7 +152,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function showFreeform(
-    attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'toggle-chat' | 'toggle-tags' | 'tag',
+    attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse' | 'toggle-chat' | 'toggle-tags' | 'tag',
     checkResult?: boolean,
     persist?: boolean
 ) {
@@ -163,7 +163,7 @@ ScriptingGlobals.add(function showFreeform(
     }
 
     // prettier-ignore
-    const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down'| 'toggle-chat' | 'toggle-tags' | 'tag', 
+    const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse'| 'toggle-chat' | 'toggle-tags' | 'tag', 
     { 
         waitForRender?: boolean; 
         checkResult: (doc: Doc) => boolean; 
@@ -214,13 +214,9 @@ ScriptingGlobals.add(function showFreeform(
             checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
             setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
         }],
-        ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
-            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
-        }],
-        ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
-            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
+        ['reverse', {
+            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_reverse"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"] = !doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"]; },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => SnappingManager.ChatVisible,
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index cb0831d3c..5315612e1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -291,7 +291,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             this.askGPTPhonemes(this._inputValue);
             this._renderSide = this.backKey;
             this._outputValue = '';
-        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC);
     };
 
     onPointerMove = ({ movementX }: PointerEvent) => {
@@ -511,7 +511,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      */
     askGPT = async (callType: GPTCallType) => {
         const questionText = this.frontText;
-        const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
+        const queryText = questionText + (callType == GPTCallType.QUIZDOC ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
 
         this.loading = true;
         const res = !this.frontText
@@ -522,7 +522,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                           case GPTCallType.CHATCARD:
                               DocCast(this.dataDoc[this.backKey])[DocData].text = resp;
                               break;
-                          case GPTCallType.QUIZ:
+                          case GPTCallType.QUIZDOC:
                               this._renderSide = this.backKey;
                               this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                               break;
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index b2b0c9aea..2ed808622 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -22,6 +22,7 @@ import { ChatCompletionMessageParam } from 'openai/resources';
 import { Doc } from '../../../../../fields/Doc';
 import { parsedDoc } from '../chatboxcomponents/ChatBox';
 import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 //import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
 
 dotenv.config();
@@ -61,7 +62,7 @@ export class Agent {
         history: () => string,
         csvData: () => { filename: string; id: string; text: string }[],
         addLinkedUrlDoc: (url: string, id: string) => void,
-        createImage: (result: any, options: DocumentOptions) => void,
+        createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void,
         addLinkedDoc: (doc: parsedDoc) => Doc | undefined,
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         createCSVInDash: (url: string, title: string, id: string, data: string) => void
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 16da360fc..6e9307d37 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -42,6 +42,7 @@ import './ChatBox.scss';
 import MessageComponentBox from './MessageComponent';
 import { ProgressBar } from './ProgressBar';
 import { OpenWhere } from '../../OpenWhere';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 
 dotenv.config();
 
@@ -412,7 +413,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         });
 
     @action
-    createImageInDash = async (result: any, options: DocumentOptions) => {
+    createImageInDash = async (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => {
         const newImgSrc =
             result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
                 ? ClientUtils.prepend(result.accessPaths.agnostic.client)
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index 177552c5c..dc6140871 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -1,10 +1,10 @@
-import { v4 as uuidv4 } from 'uuid';
 import { RTFCast } from '../../../../../fields/Types';
 import { DocumentOptions } from '../../../../documents/Documents';
 import { Networking } from '../../../../Network';
 import { ParametersType, ToolInfo } from '../types/tool_types';
 import { Observation } from '../types/types';
 import { BaseTool } from './BaseTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 
 const imageCreationToolParams = [
     {
@@ -25,8 +25,8 @@ const imageCreationToolInfo: ToolInfo<ImageCreationToolParamsType> = {
 };
 
 export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
-    private _createImage: (result: any, options: DocumentOptions) => void;
-    constructor(createImage: (result: any, options: DocumentOptions) => void) {
+    private _createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void;
+    constructor(createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void) {
         super(imageCreationToolInfo);
         this._createImage = createImage;
     }
@@ -42,23 +42,19 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
             });
             console.log('Image generation result:', result);
             this._createImage(result, { text: RTFCast(image_prompt) });
-            if (url) {
-                const id = uuidv4();
-
-                return [
-                    {
-                        type: 'image_url',
-                        image_url: { url },
-                    },
-                ];
-            } else {
-                return [
-                    {
-                        type: 'text',
-                        text: `An error occurred while generating image.`,
-                    },
-                ];
-            }
+            return url
+                ? [
+                      {
+                          type: 'image_url',
+                          image_url: { url },
+                      },
+                  ]
+                : [
+                      {
+                          type: 'text',
+                          text: `An error occurred while generating image.`,
+                      },
+                  ];
         } catch (error) {
             console.log(error);
             return [
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 72381cfad..cb3e9b2d7 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -10,7 +10,7 @@ import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { DescriptionSeperator, DocSeperator, GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -18,7 +18,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { undoable } from '../../../util/UndoManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { TagItem } from '../../TagsView';
-import { docSortings } from '../../collections/CollectionSubView';
+import { ChatSortField, docSortings } from '../../collections/CollectionSubView';
 import { DocumentView } from '../../nodes/DocumentView';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
@@ -36,6 +36,7 @@ export enum GPTPopupMode {
 export class GPTPopup extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
+    static ChatTag = '#chat'; // tag used by GPT popup to filter docs
     private _messagesEndRef: React.RefObject<HTMLDivElement>;
     private _correlatedColumns: string[] = [];
     private _dataChatPrompt: string | undefined = undefined;
@@ -77,10 +78,10 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 if (hasChildDocs) {
                     this._textToDocMap.clear();
                     this.setCollectionContext(selDoc.Document);
-                    this.onGptResponse = (sortResult: string, questionType: GPTTypeStyle, tag?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, tag);
+                    this.onGptResponse = (sortResult: string, questionType: GPTDocCommand, args?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, args);
                     this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
                     this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
-                            Doc.getDescription(doc).then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+                            Doc.getDescription(doc).then(text => this._textToDocMap.set(text.trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
                         )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
                 }
             },
@@ -110,8 +111,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
     @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
 
     onQuizRandom?: () => void;
-    onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
-    questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
+    onGptResponse?: (sortResult: string, questionType: GPTDocCommand, args?: string) => void;
+    NumberToCommandType = (questionType: string) => +questionType.split(' ')[0][0];
 
     /**
      * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
@@ -120,44 +121,38 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * @param questionType
      * @param tag
      */
-    processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
+    processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand, args?: string) =>
         undoable(() => {
-            // Split the string into individual list items
-            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
-            if (questionType === GPTTypeStyle.Filter) {
-                docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, '#chat'));
-            }
-
-            if (questionType === GPTTypeStyle.SortDocs) {
-                docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
-            }
-
-            listItems.forEach((item, index) => {
-                const normalizedItem = item.replace(/\n/g, ' ').trim();
-                // find the corresponding Doc in the textToDoc map
-                const doc = textToDocMap.get(normalizedItem);
-                if (doc) {
+            switch (questionType) { // reset collection based on question typefc
+                case GPTDocCommand.Sort:
+                    docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
+                    break;
+                case GPTDocCommand.Filter:
+                    docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag));
+                    break;
+            } // prettier-ignore
+
+            gptOutput.split('======').filter(item => item.trim() !== '')  // Split output into individual document contents
+                .map(docContentRaw => textToDocMap.get(docContentRaw.replace(/\n/g, ' ').trim())) // the find the corresponding Doc using textToDoc map
+                .filter(doc => doc).map(doc => doc!) // filter out undefined values
+                .forEach((doc, index) => {
                     switch (questionType) {
-                        case GPTTypeStyle.SortDocs:
-                            doc.chatIndex = index;
+                        case GPTDocCommand.Sort:
+                            doc[ChatSortField] = index;
                             break;
-                        case GPTTypeStyle.AssignTags:
-                            if (tag) {
-                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
-                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+                        case GPTDocCommand.AssignTags:
+                            if (args) {
+                                const hashTag = args.startsWith('#') ? args : '#' + args[0].toLowerCase() + args.slice(1);
+                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(args)) ?? hashTag;
                                 TagItem.addTagToDoc(doc, filterTag);
                             }
                             break;
-                        case GPTTypeStyle.Filter:
-                            TagItem.addTagToDoc(doc, '#chat');
-                            Doc.setDocFilter(docView.Document, 'tags', '#chat', 'check');
+                        case GPTDocCommand.Filter:
+                            TagItem.addTagToDoc(doc, GPTPopup.ChatTag);
+                            Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check');
                             break;
                     }
-                } else {
-                    console.warn(`No matching document found for item: ${normalizedItem}`);
-                }
-            });
+                }); // prettier-ignore
         }, '')();
 
     /**
@@ -173,7 +168,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         StrCast(doc.gptRubric)
             ? Promise.resolve(StrCast(doc.gptRubric))
             : Doc.getDescription(doc).then(desc =>
-                  gptAPICall(desc, GPTCallType.RUBRIC)
+                  gptAPICall(desc, GPTCallType.MAKERUBRIC)
                       .then(res => (doc.gptRubric = res))
                       .catch(err => console.error('GPT call failed', err))
               );
@@ -191,7 +186,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     `Question: ${desc}; 
                     UserAnswer: ${quizAnswer};
                     Rubric: ${StrCast(doc.gptRubric)}`,
-                    GPTCallType.QUIZ
+                    GPTCallType.QUIZDOC
                 ).then(res => {
                     this._conversationArray.push(res || 'GPT provided no answer');
                     this.onQuizRandom?.();
@@ -205,20 +200,20 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * @param userPrompt the user's input that chat will respond to
      */
     generateUserPromptResponse = (userPrompt: string) =>
-        gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+        gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then((commandType, args = commandType.split(' ').slice(1).join(' ')) =>
             (async () => {
-                switch (this.questionTypeNumberToStyle(questionType)) {
-                    case GPTTypeStyle.AssignTags:
-                    case GPTTypeStyle.Filter:    return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SUBSET, userPrompt)) ?? "";
-                    case GPTTypeStyle.SortDocs:  return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SORT, userPrompt)) ?? "";
-                    default:                     return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(desc, GPTCallType.INFO, userPrompt));
+                switch (this.NumberToCommandType(commandType)) {
+                    case GPTDocCommand.AssignTags:
+                    case GPTDocCommand.Filter:    return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SUBSETDOCS, descs)) ?? "";
+                    case GPTDocCommand.Sort:      return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SORTDOCS, descs)) ?? "";
+                    default:                      return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(userPrompt, GPTCallType.DOCINFO, desc));
                 } // prettier-ignore
             })().then(
                 action(res => {
                     // Trigger the callback with the result
-                    this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
+                    this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType), args);
                     this._conversationArray.push(
-                        [GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType)) ? res:
+                        this.NumberToCommandType(commandType) === GPTDocCommand.GetInfo ? res:
                         // Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom
                         (res.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`)) ?? [])[1]?.trim() ?? 'No explanation found'
                     );
@@ -583,10 +578,10 @@ export class GPTPopup extends ObservableReactComponent<object> {
                         tooltip="Clear Chat filter"
                         toggleType={ToggleType.BUTTON}
                         type={Type.PRIM}
-                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat')}
-                        text={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'filtered' : ''}
-                        color={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'red' : 'transparent'}
-                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', '#chat', 'remove')}
+                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag)}
+                        text={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'filtered' : ''}
+                        color={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'red' : 'transparent'}
+                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')}
                     />
                     {(this._mode === GPTPopupMode.USER_PROMPT || this._mode === GPTPopupMode.QUIZ_RESPONSE) && (
                         <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} />
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 950d9047c..bdd41b0bb 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1482,7 +1482,7 @@ export namespace Doc {
                 case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '')
                                                 .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.'));
                 case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text;
-                default:               return StrCast(tdoc.title);
+                default:               return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
         }}); // prettier-ignore
         return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text));
     }
-- 
cgit v1.2.3-70-g09d2

