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
8 changes: 0 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ GOOGLE_GENERATIVE_AI_API_KEY=AIza...
# Get your API key from firecrawl.dev
FIRECRAWL_API_KEY=fc_...

# Scraping Mode
# Options:
# - hybrid (Default: Try Google Context -> Fallback to Firecrawl if key exists -> Fallback to Direct)
# - firecrawl-only (Force Firecrawl for all scraping)
# - google-only (Force Google Context only)
# - direct-only (Force Direct Fetch only)
SCRAPING_MODE=hybrid

# FastAPI Service (optional - file conversion, doc-to-markdown, audio/video analysis)
# FASTAPI_BASE_URL=https://your-fastapi-service.com
# FASTAPI_API_KEY=your-service-api-key
Expand Down
69 changes: 5 additions & 64 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import { getPostHogServerClient } from "@/lib/posthog-server";
import { withServerObservability } from "@/lib/with-server-observability";
import { normalizeLegacyToolMessages } from "@/lib/ai/legacy-tool-message-compat";

// Regex patterns as constants (compiled once, reused for all requests)
const URL_CONTEXT_REGEX = /\[URL_CONTEXT:(.+?)\]/g;
const DIRECT_URL_REGEX = /https?:\/\/[^\s]+/g;

/**
* Extract workspaceId from system context or request body
*/
Expand All @@ -41,50 +37,20 @@ function extractWorkspaceId(body: any): string | null {
}

/**
* Process messages in a single pass: extract URL context URLs and clean markers
* Process messages in a single pass: clean URL context markers
* File attachments are handled natively as file parts via the SupabaseAttachmentAdapter.
*/
function processMessages(messages: any[]): {
urlContextUrls: string[];
cleanedMessages: any[];
} {
const urlContextUrlsSet = new Set<string>();

const cleanedMessages = messages.map((message) => {
if (message.content && Array.isArray(message.content)) {
const updatedContent = message.content.map((part: any) => {
Comment on lines 43 to 48

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 issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the relevant section
if [ -f "src/app/api/chat/route.ts" ]; then
  echo "=== File found, reading lines 40-55 ==="
  sed -n '40,55p' src/app/api/chat/route.ts
else
  echo "File not found, listing directory structure"
  find . -name "route.ts" -type f | head -20
fi

Repository: ThinkEx-OSS/thinkex

Length of output: 775


🏁 Script executed:

# Search for eslint configuration files
echo "=== Looking for eslint config ==="
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 2>/dev/null | head -10

Repository: ThinkEx-OSS/thinkex

Length of output: 116


🏁 Script executed:

# Look for SDK type definitions or interfaces related to messages/content
echo "=== Searching for message/content type definitions ==="
rg -i "interface.*message|type.*message|interface.*content|type.*content" --type ts --type tsx -A 3 | head -100

Repository: ThinkEx-OSS/thinkex

Length of output: 162


🏁 Script executed:

# Read eslint config to verify no-explicit-any rule
echo "=== Eslint config ==="
cat eslint.config.mjs

Repository: ThinkEx-OSS/thinkex

Length of output: 565


🏁 Script executed:

# Search for type definitions more broadly
echo "=== Looking for message types ==="
rg -i "message|content" --type ts -A 2 src/app/api/chat/route.ts | head -80

Repository: ThinkEx-OSS/thinkex

Length of output: 2642


🏁 Script executed:

# Check imports and type definitions in the chat route file
echo "=== Full file header and context ==="
head -100 src/app/api/chat/route.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 3115


Replace any types with SDK message types.

Lines 43, 44, and 48 use any which violates the enabled @typescript-eslint/no-explicit-any rule. Use UIMessage[] or the appropriate SDK content types from the "ai" package instead.

🧰 Tools
🪛 ESLint

[error] 43-43: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 44-44: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 48-48: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/chat/route.ts` around lines 43 - 48, Replace the explicit any
types in processMessages with the SDK message types: change the function
signature to accept messages: UIMessage[] (or the equivalent SDK type from the
"ai" package) and type cleanedMessages and updatedContent accordingly (e.g.,
UIMessage[] and the proper content part type from the SDK). Update the local
variable declarations that reference message, message.content, and part to use
the SDK types instead of any so they satisfy `@typescript-eslint/no-explicit-any`
and align with the "ai" package types.

if (part.type === "text" && typeof part.text === "string") {
const text = part.text;

// Extract URL context URLs (use Set for O(1) lookups)
const urlContextRegexLocal = new RegExp(
URL_CONTEXT_REGEX.source,
URL_CONTEXT_REGEX.flags,
);
const urlContextMatches = text.matchAll(urlContextRegexLocal);
for (const urlMatch of urlContextMatches) {
const url = urlMatch[1];
if (url) urlContextUrlsSet.add(url);
}

// Extract direct URLs
const directUrlRegexLocal = new RegExp(
DIRECT_URL_REGEX.source,
DIRECT_URL_REGEX.flags,
);
const directUrlMatches = text.matchAll(directUrlRegexLocal);
for (const directMatch of directUrlMatches) {
const url = directMatch[0];
if (url) urlContextUrlsSet.add(url);
}

// Clean URL_CONTEXT markers (create new instance to avoid global regex state issues)
const urlContextReplaceRegex = new RegExp(
URL_CONTEXT_REGEX.source,
URL_CONTEXT_REGEX.flags,
);
const updatedText = text.replace(
urlContextReplaceRegex,
/\[URL_CONTEXT:(.+?)\]/g,
(_match: string, url: string) => {
return url;
},
Expand All @@ -100,7 +66,6 @@ function processMessages(messages: any[]): {
});

return {
urlContextUrls: Array.from(urlContextUrlsSet),
cleanedMessages,
};
}
Expand Down Expand Up @@ -166,26 +131,6 @@ function injectSelectionContext(
}
}

/**
* Build the enhanced system prompt with guidelines and detection hints
* Uses array join for better performance than string concatenation
*/
function buildSystemPrompt(
baseSystem: string,
urlContextUrls: string[],
): string {
const parts: string[] = [baseSystem];

// Add URL detection hint if URLs are present
if (urlContextUrls.length > 0) {
parts.push(
`\n\nURL DETECTION: The user's message contains ${urlContextUrls.length} URL(s): ${urlContextUrls.join(", ")}. Call the processUrls tool with a structured object like { "urls": ["https://example.com"] } to analyze them. Do not wrap the URLs in a JSON string.`,
);
}

return parts.join("");
}

async function handlePOST(req: Request) {
let workspaceId: string | null = null;
let activeFolderId: string | undefined;
Expand Down Expand Up @@ -261,9 +206,8 @@ async function handlePOST(req: Request) {
emptyMessages: "remove",
});

// Process messages in single pass: extract URLs and clean markers
const { urlContextUrls, cleanedMessages } =
processMessages(convertedMessages);
// Process messages in single pass: clean URL context markers
const { cleanedMessages } = processMessages(convertedMessages);

// Get pre-formatted selected cards context from client (no DB fetch needed)
const selectedCardsContext = getSelectedCardsContext(body);
Expand All @@ -282,9 +226,6 @@ async function handlePOST(req: Request) {
modelId = `anthropic/${modelId}`;
}

// Build system prompt (identity, guidelines, URL hints — no selected cards)
const finalSystemPrompt = buildSystemPrompt(system, urlContextUrls);

// Inject selected cards + reply selections into the last user message
injectSelectionContext(
cleanedMessages,
Expand Down Expand Up @@ -355,7 +296,7 @@ async function handlePOST(req: Request) {
const result = streamText({
model: model,
temperature: 1.0,
system: finalSystemPrompt,
system,
messages: cleanedMessages,
stopWhen: stepCountIs(25),
tools,
Expand Down
5 changes: 3 additions & 2 deletions src/app/api/workspaces/autogen/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { db, workspaces } from "@/lib/db/client";
import { generateSlug } from "@/lib/workspace/slug";
import { workspaceWorker, type CreateItemParams } from "@/lib/ai/workers";
import { searchVideos } from "@/lib/youtube";
import { UrlProcessor } from "@/lib/ai/utils/url-processor";
import { FirecrawlClient } from "@/lib/ai/utils/firecrawl";
import { findNextAvailablePosition } from "@/lib/workspace-state/grid-layout-helpers";
import { generateItemId } from "@/lib/workspace-state/item-helpers";
import type { Item, QuizQuestion } from "@/lib/workspace-state/types";
Expand Down Expand Up @@ -173,7 +173,8 @@ async function runDistillationAgent(
let linkContext = "";
if (nonYtLinks.length > 0) {
send({ type: "toolCall", data: { toolName: "urlFetch", status: "fetching" } });
const results = await UrlProcessor.processUrls(nonYtLinks);
const client = new FirecrawlClient();
const results = await client.scrapeUrls(nonYtLinks);
const successful = results.filter((r) => r.success && r.content);
if (successful.length > 0) {
linkContext =
Expand Down
73 changes: 19 additions & 54 deletions src/components/assistant-ui/URLContextToolUI.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { LinkIcon, ChevronDownIcon, CheckIcon, ExternalLinkIcon, AlertCircleIcon } from "lucide-react";
import { LinkIcon, ChevronDownIcon, CheckIcon, ExternalLinkIcon } from "lucide-react";
import {
useCallback,
useRef,
Expand Down Expand Up @@ -204,25 +204,17 @@ type URLMetadata = {
urlRetrievalStatus?: string;
};

type SourceMetadata = {
uri?: string;
title?: string;
};

type ProcessUrlsResult =
| string
| {
text: string;
metadata?: {
urlMetadata?: URLMetadata[] | null;
groundingChunks?: unknown[] | null;
sources?: SourceMetadata[] | null;
};
};

export const URLContextToolUI = makeAssistantToolUI<{
urls?: string[];
instruction?: string;
jsonInput?: string;
}, ProcessUrlsResult>({
toolName: "processUrls",
Expand All @@ -231,17 +223,19 @@ export const URLContextToolUI = makeAssistantToolUI<{
const isComplete = status.type === "complete";

const parsedResult = result != null ? parseURLContextResult(result) : null;
type Meta = { urlMetadata?: URLMetadata[]; groundingChunks?: unknown[]; sources?: SourceMetadata[] };
type Meta = {
urlMetadata?: URLMetadata[];
};
const metadata = (typeof parsedResult === "object" && parsedResult !== null && "metadata" in parsedResult ? (parsedResult as { metadata?: Meta }).metadata : null) as Meta | null;
const urlMetadata = metadata?.urlMetadata ?? null;
const groundingChunks = metadata?.groundingChunks ?? null;
const sources = metadata?.sources ?? null;

const normalizedArgs = normalizeProcessUrlsArgs(args);
const urls = normalizedArgs?.urls ?? [];
const instruction = normalizedArgs?.instruction;

const urlCount = urls.length;
const successfulCount =
urlMetadata?.filter((m) => m.urlRetrievalStatus === "URL_RETRIEVAL_STATUS_SUCCESS").length ?? 0;
const failedCount = (urlMetadata?.length ?? 0) - successfulCount;

// Helper to get status badge color
const getStatusColor = (status: string) => {
Expand All @@ -265,7 +259,7 @@ export const URLContextToolUI = makeAssistantToolUI<{
<ToolRoot>
<ToolTrigger
active={isRunning}
label={isRunning ? "Processing URLs" : "URLs processed"}
label={isRunning ? "Processing links" : "Links processed"}
icon={<LinkIcon className="aui-tool-trigger-icon size-4 shrink-0" />}
/>

Expand All @@ -274,23 +268,12 @@ export const URLContextToolUI = makeAssistantToolUI<{
<div className="space-y-3">
{urlCount > 0 && (
<div>
<span className="text-xs font-medium text-muted-foreground/70">URLs:</span>
{instruction && (
<div className="mt-1 mb-2 p-2 bg-muted/50 rounded-md border border-border/50">
<div className="flex items-start gap-2">
<AlertCircleIcon className="h-3 w-3 shrink-0 mt-0.5 text-muted-foreground/60" />
<div className="flex-1">
<span className="text-xs font-medium text-muted-foreground/70">Custom instruction:</span>
<p className="text-xs text-foreground mt-0.5">{instruction}</p>
</div>
</div>
</div>
)}
<span className="text-xs font-medium text-muted-foreground/70">Links:</span>
<div className="mt-1 space-y-1">
{urls.map((url, index) => {
{urls.map((url) => {
const urlMeta = urlMetadata?.find((m) => m.retrievedUrl === url);
return (
<div key={index} className="flex flex-col gap-1">
<div key={url} className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<ExternalLinkIcon className="h-3 w-3 shrink-0 text-muted-foreground/60" />
<a
Expand Down Expand Up @@ -323,7 +306,7 @@ export const URLContextToolUI = makeAssistantToolUI<{
<div className="flex items-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-500" />
<span className="text-xs text-foreground">
Analyzing {urlCount} URL{urlCount !== 1 ? "s" : ""}...
Processing {urlCount} link{urlCount !== 1 ? "s" : ""}...
</span>
</div>
)}
Expand All @@ -333,34 +316,16 @@ export const URLContextToolUI = makeAssistantToolUI<{
<div className="flex items-center gap-2">
<CheckIcon className="h-4 w-4 text-green-500" />
<span className="text-xs text-foreground">
Successfully processed {urlCount} URL{urlCount !== 1 ? "s" : ""}
Processed {urlCount} link{urlCount !== 1 ? "s" : ""}
</span>
</div>

{metadata && (
<div className="space-y-2 border-t pt-2">
{groundingChunks && groundingChunks.length > 0 && (
<div className="text-xs">
<span className="font-medium text-muted-foreground/70">Grounding chunks: </span>
<span className="text-foreground">{groundingChunks.length}</span>
</div>
)}

{sources && sources.length > 0 && (
<div className="text-xs">
<span className="font-medium text-muted-foreground/70">Sources: </span>
<span className="text-foreground">{sources.length}</span>
</div>
)}

{urlMetadata && urlMetadata.length > 0 && (
<div className="text-xs">
<span className="font-medium text-muted-foreground/70">Retrieved: </span>
<span className="text-foreground">
{urlMetadata.filter((m) => m.urlRetrievalStatus === "URL_RETRIEVAL_STATUS_SUCCESS").length} / {urlMetadata.length}
</span>
</div>
)}
{metadata && urlMetadata && urlMetadata.length > 0 && (
<div className="border-t pt-2">
<Badge variant="outline" className="text-[10px] px-1.5 py-0.5">
{successfulCount} loaded
{failedCount > 0 ? `, ${failedCount} unavailable` : ""}
</Badge>
</div>
)}
</div>
Expand Down
Loading
Loading