Skip to content
Merged
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
75 changes: 74 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,80 @@ async function submit(formData?: FormData, skip?: boolean) {
const maxMessages = useSpecificAPI ? 5 : 10
messages.splice(0, Math.max(messages.length - maxMessages, 0))

const userInput = skip ? `{"action": "skip"}` : formData?.get('input') as string
const userInput = skip
? `{"action": "skip"}`
: (formData?.get('input') as string);

if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') {
const definition = userInput.toLowerCase().trim() === 'what is a planet computer?'
? "A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet"
: "QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery";

Comment on lines +55 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Null-safe normalization: prevent runtime on file-only or non-text submissions

userInput can be null/undefined (e.g., file-only requests). Calling .toLowerCase() will throw. Compute a normalized string once and guard against skip. Also avoid repeating .toLowerCase().trim().

Apply:

-  const userInput = skip
-    ? `{"action": "skip"}`
-    : (formData?.get('input') as string);
-
-  if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') {
-    const definition = userInput.toLowerCase().trim() === 'what is a planet computer?'
+  const rawInput = skip ? null : formData?.get('input')
+  const userInput = typeof rawInput === 'string' ? rawInput : ''
+  const normalizedInput = userInput.toLowerCase().trim()
+
+  if (!skip && (normalizedInput === 'what is a planet computer?' || normalizedInput === 'what is qcx-terra?')) {
+    const definition = normalizedInput === 'what is a planet computer?'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userInput = skip
? `{"action": "skip"}`
: (formData?.get('input') as string);
if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') {
const definition = userInput.toLowerCase().trim() === 'what is a planet computer?'
? "A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet"
: "QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery";
const rawInput = skip ? null : formData?.get('input')
const userInput = typeof rawInput === 'string' ? rawInput : ''
const normalizedInput = userInput.toLowerCase().trim()
if (!skip && (normalizedInput === 'what is a planet computer?' || normalizedInput === 'what is qcx-terra?')) {
const definition = normalizedInput === 'what is a planet computer?'
? "A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet"
: "QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery";
🤖 Prompt for AI Agents
In app/actions.tsx around lines 55 to 63, the code calls
userInput.toLowerCase().trim() which will throw when userInput is null/undefined
(e.g., file-only submissions); compute a single null-safe normalized string
(e.g., const normalized = (userInput ?? '').toString().toLowerCase().trim())
after handling skip, use that normalized variable in the conditional and
ternary, and ensure you short-circuit when skip is true so you don't attempt
normalization on undefined input.

const content = JSON.stringify(Object.fromEntries(formData!));
const type = 'input';

Comment on lines +59 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential runtime crash: userInput.toLowerCase() is called before verifying userInput is a non-empty string. If formData is absent or input is missing (e.g., null), this will throw. Also, toLowerCase().trim() is duplicated; normalize once and reuse. Consider moving to a safe normalization and using a small lookup table for hardcoded answers to simplify and future-proof this logic.

Suggestion

Consider refactoring this block to safely normalize the input and to use a lookup for hardcoded responses. This also simplifies the ternary and avoids repeated normalization:

const normalizedInput = (typeof userInput === 'string' ? userInput : '').toLowerCase().trim();
const HARD_CODED_ANSWERS: Record<string, string> = {
  'what is a planet computer?': 'A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet',
  'what is qcx-terra?': 'QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery',
};

if (HARD_CODED_ANSWERS[normalizedInput]) {
  const definition = HARD_CODED_ANSWERS[normalizedInput];

  const content = JSON.stringify({ input: userInput ?? '' });
  const type = 'input';

  aiState.update({
    ...aiState.get(),
    messages: [
      ...aiState.get().messages,
      { id: nanoid(), role: 'user', content, type },
    ],
  });

  const definitionStream = createStreamableValue();
  definitionStream.done(definition);

  uiStream.append(
    <Section title="response">
      <BotMessage content={definitionStream.value} />
    </Section>
  );

  const groupId = nanoid();
  const relatedQueries = { items: [] };

  aiState.done({
    ...aiState.get(),
    messages: [
      ...aiState.get().messages,
      { id: groupId, role: 'assistant', content: definition, type: 'response' },
      { id: groupId, role: 'assistant', content: JSON.stringify(relatedQueries), type: 'related' },
      { id: groupId, role: 'assistant', content: 'followup', type: 'followup' },
    ],
  });

  isGenerating.done(false);
  uiStream.done();

  return {
    id: groupId,
    isGenerating: isGenerating.value,
    component: uiStream.value,
    isCollapsed: isCollapsed.value,
  };
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor.

Comment on lines +64 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formData! is unsafe here and JSON.stringify(Object.fromEntries(formData)) can silently drop non-serializable values (e.g., File becomes {}). This can produce misleading content for the stored user message or even throw if formData is undefined. Consider constructing the content safely from known primitives (e.g., the input string).

Suggestion

Prefer stable serialization and avoid the non-null assertion:

const content = JSON.stringify({ input: userInput ?? '' });
const type = 'input';

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this fix.

aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: nanoid(),
role: 'user',
content,
type,
},
],
});

Comment on lines +64 to +79
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not persist entire FormData; store only the minimal input field

Serializing Object.fromEntries(formData!) risks leaking unrelated fields (and file metadata) into persisted chat messages and can yield {} for File values. Persist only { input } to align with getUIStateFromAIState expectations and minimize data retention.

-    const content = JSON.stringify(Object.fromEntries(formData!));
+    // Only persist minimal structured input to avoid leaking other form fields/files
+    const content = JSON.stringify({ input: userInput });
     const type = 'input';

Committable suggestion skipped: line range outside the PR's diff.

const definitionStream = createStreamableValue();
definitionStream.done(definition);

const answerSection = (
<Section title="response">
<BotMessage content={definitionStream.value} />
</Section>
);

uiStream.append(answerSection);

Comment on lines +89 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

UI parity: append Follow-up section in the early path

General flow appends a Follow-up section to the stream; the early path should too for consistent UX.

-    uiStream.append(answerSection);
+    uiStream.append(answerSection);
+    uiStream.append(
+      <Section title="Follow-up">
+        <FollowupPanel />
+      </Section>
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uiStream.append(answerSection);
uiStream.append(answerSection);
uiStream.append(
<Section title="Follow-up">
<FollowupPanel />
</Section>
);
🤖 Prompt for AI Agents
In app/actions.tsx around lines 89-90, the early return path appends the
answerSection to uiStream but omits appending the Follow-up section, causing UI
inconsistency; after the existing uiStream.append(answerSection) in the early
path, create or obtain the same followUpSection used in the normal flow and call
uiStream.append(followUpSection) there (ensure you reuse the same
structure/props to avoid duplication or mismatch and guard against
null/undefined before appending).

const groupeId = nanoid();
const relatedQueries = { items: [] };

Comment on lines +91 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid redeclaring groupeId

groupeId is already defined earlier (Line 50). Redeclaration here shadows it unnecessarily.

-    const groupeId = nanoid();
-    const relatedQueries = { items: [] };
+    const relatedQueries = { items: [] };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const groupeId = nanoid();
const relatedQueries = { items: [] };
const relatedQueries = { items: [] };
🤖 Prompt for AI Agents
In app/actions.tsx around lines 91 to 93, the code redeclares "groupeId" which
was already defined at line 50, causing an unnecessary shadow; remove the "const
groupeId = nanoid();" declaration and either reuse the previously declared
variable (assign to it or just reference it) or, if a distinct new id is
required, rename this variable to a different, descriptive identifier (e.g.,
newGroupId) to avoid shadowing and keep naming consistent.

aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: groupeId,
role: 'assistant',
content: definition,
type: 'response',
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify(relatedQueries),
type: 'related',
},
{
id: groupeId,
role: 'assistant',
content: 'followup',
type: 'followup',
},
],
Comment on lines +99 to +116
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three assistant messages use the same id (groupeId). If id is used as a unique identifier (common in React lists/state), this will cause collisions and potentially break rendering or state updates. Use a unique id per message and, if grouping is needed, add a separate groupId property instead of reusing id.

Suggestion

Assign unique IDs to each message:

aiState.done({
  ...aiState.get(),
  messages: [
    ...aiState.get().messages,
    {
      id: nanoid(),
      role: 'assistant',
      content: definition,
      type: 'response',
    },
    {
      id: nanoid(),
      role: 'assistant',
      content: JSON.stringify(relatedQueries),
      type: 'related',
    },
    {
      id: nanoid(),
      role: 'assistant',
      content: 'followup',
      type: 'followup',
    },
  ],
});

If you prefer grouping semantics, we can add a groupId field while keeping id unique. Reply with "@CharlieHelps yes please" and I’ll prepare the commit.

});

isGenerating.done(false);
uiStream.done();

return {
id: nanoid(),
isGenerating: isGenerating.value,
component: uiStream.value,
isCollapsed: isCollapsed.value,
};
Comment on lines +122 to +127
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id returned from this early-return block is a fresh nanoid() that does not match the groupeId used for the assistant messages. This likely breaks grouping/association between the UI component and the messages for that interaction.

Suggestion

Return the same group id you used for the assistant messages to keep grouping consistent, e.g.:

return {
  id: groupeId, // or `groupId` if you rename it
  isGenerating: isGenerating.value,
  component: uiStream.value,
  isCollapsed: isCollapsed.value,
};

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that wires this up consistently.

}
const file = !skip ? (formData?.get('file') as File) : undefined

if (!userInput && !file) {
Expand Down