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
21 changes: 20 additions & 1 deletion src/app/api/cards/from-message/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,25 @@ export async function POST(request: NextRequest) {
const userId = session.user.id;

const body = await request.json();
const { content, workspaceId, folderId } = body;
const { content, workspaceId, folderId, sources } = body;
Comment thread
urjitc marked this conversation as resolved.

if (sources !== undefined) {
const hasInvalidSource =
!Array.isArray(sources) ||
sources.some(
(source) =>
!source ||
typeof source.title !== "string" ||
typeof source.url !== "string"
);

if (hasInvalidSource) {
return NextResponse.json(
{ error: "Sources must be an array of { title, url } objects" },
{ status: 400 }
);
}
}



Expand Down Expand Up @@ -104,6 +122,7 @@ Return ONLY the reformatted note content in markdown format. Do not include any
workspaceId,
title,
content: cleanedContent,
sources,
folderId,
});

Expand Down
77 changes: 76 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,83 @@ If the user asks to "add a youtube video" or "search for a video" but does not p
CONFIDENCE THRESHOLD:
If you are uncertain about a fact's accuracy or currency, prefer to search rather than risk providing outdated information.


CITATION REQUIREMENT:
When using search results (grounding), you must include the date of each article/source if available.`);
When using search results (grounding), you must include the date of each article/source if available.

CRITICAL: USE WEB SEARCH TOOL FOR RESEARCH-BASED NOTES:
When the user asks you to create or update a note about a topic that requires current information or research (e.g., "India China relations", "latest AI trends", "recent developments in..."), you MUST:
1. FIRST call the webSearch tool to gather information
2. THEN create/update the note using that information
3. Extract sources from the webSearch tool result

This is MANDATORY because automatic grounding does not provide source URLs for attribution.

SOURCE EXTRACTION REQUIREMENT - CRITICAL:
When creating OR updating a note, you MUST ALWAYS extract and pass sources using the 'sources' parameter.

WHEN TO EXTRACT SOURCES:
1. **Web Search Tool Results**: When you call webSearch, extract sources from the grounding metadata in the response
- Example prompts that REQUIRE webSearch: "latest AI trends", "India China relations", "recent developments in...", any topic-based research
- The webSearch tool returns groundingMetadata with sources - YOU MUST EXTRACT THESE

2. **User-Provided URLs**: If the user provided a URL that you read/analyzed (via processUrls tool)
- Example: "Summarize https://example.com" → MUST include example.com as a source

HOW TO EXTRACT SOURCES FROM WEBSEARCH:
The webSearch tool returns a JSON string. You MUST parse it correctly to extract REAL URLs, not make them up!

Structure of the response:
{
"text": "...",
"groundingMetadata": {
"groundingChunks": [
{
"web": {
"uri": "https://actual-real-url.com/article", // ← EXTRACT THIS
"title": "Actual Page Title" // ← EXTRACT THIS
}
}
]
}
}

PARSING CODE EXAMPLE:
const result = await webSearch("India China relations");
const parsed = JSON.parse(result);
const chunks = parsed.groundingMetadata?.groundingChunks || [];
const sources = chunks.map(chunk => ({
title: chunk.web?.title || "Untitled",
url: chunk.web?.uri || ""
})).filter(s => s.url);

CRITICAL: You MUST extract chunk.web.uri for the URL. DO NOT make up URLs. DO NOT hallucinate URLs.
If groundingChunks is missing or empty, skip source extraction for that query.

HANDLING REDIRECT URLs:
⚠️ IMPORTANT: Some chunk.web.uri values may contain temporary redirect URLs like "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."

Do NOT construct URLs from titles or domains. Do NOT guess. Use chunk.web.uri as provided.
If a redirect URL is the only available source, include it rather than dropping all sources.

NOTE CONTENT RULES:
🚫 DO NOT include sources, references, or citations in the note content itself.
🚫 DO NOT add "Sources:", "References:", or "Citations:" sections to the markdown.
The sources parameter will be displayed separately by the UI. Keep note content clean and focused on the topic.

EXAMPLES:
✅ CORRECT - Creating note about "India China relations":
1. Call webSearch("India China relations current border dispute")
2. Extract sources from groundingMetadata
3. createNote/updateNote with sources: [
{ title: "India-China Border Dispute Explained", url: "https://bbc.com/news/india-china..." },
{ title: "Galwan Valley Clash 2020", url: "https://reuters.com/world/india..." }
]

❌ WRONG - Creating note without calling webSearch or providing sources:
sources: undefined // This is NOT ACCEPTABLE

This is ABSOLUTELY MANDATORY for both createNote AND updateNote tools. NO EXCEPTIONS.`);

// Add file detection hint if file URLs are present
if (fileUrls.length > 0) {
Expand Down
151 changes: 151 additions & 0 deletions src/app/api/notes/create-from-urls/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { google } from "@ai-sdk/google";
import { generateText } from "ai";
import { auth } from "@/lib/auth";
import { workspaceWorker } from "@/lib/ai/workers";
import { logger } from "@/lib/utils/logger";
import { headers } from "next/headers";
import { z } from "zod";

const createFromUrlsSchema = z.object({
urls: z.array(z.string().url()).min(1).max(10),
workspaceId: z.string().uuid(),
folderId: z.string().uuid().optional(),
});


export async function POST(req: Request) {
try {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
}

const body = await req.json();
const parseResult = createFromUrlsSchema.safeParse(body);

if (!parseResult.success) {
return new Response(JSON.stringify({ error: "Invalid request body", details: parseResult.error.flatten() }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

const { urls, workspaceId, folderId } = parseResult.data;

logger.info("📝 [API] Creating note from URLs:", { workspaceId, urlCount: urls.length });

// Use Google's URL Context API to analyze the content
const tools: any = {
url_context: google.tools.urlContext({}),
};

const promptText = `Analyze the content from the following article URL(s) and create a comprehensive study note.

URLs to analyze:
${urls.map((url, i) => `${i + 1}. ${url}`).join('\n')}

Provide your response in this exact format with clear delimiters:

===TITLE===
[Your clear, informative title here]

===CONTENT===
[Your detailed markdown content here with proper headings, bullet points, etc.]

===SOURCES===
[One source per line in format: Title | URL]

Make sure to:
- Generate a clear title that captures the main topic
- Create comprehensive markdown content synthesizing key information from all articles
- Use proper markdown formatting (headings, bullet points, etc.)
- Include all URLs in the sources section with their actual page titles`;

const { text } = await generateText({
model: google("gemini-2.5-flash"),
tools,
prompt: promptText,
});

logger.debug("📝 [API] LLM response received:", { textLength: text?.length });

// Parse the delimited response
let title = "Article Summary";
let content = "";
let sources: Array<{ title: string; url: string }> = [];

try {
const titleMatch = text.match(/===TITLE===\s*\n(.*?)(?:\n|$)/s);
if (titleMatch) {
title = titleMatch[1].trim();
}

const contentMatch = text.match(/===CONTENT===\s*\n([\s\S]*?)\n\n===/);
if (contentMatch) {
content = contentMatch[1].trim();
}

const sourcesMatch = text.match(/===SOURCES===\s*\n([\s\S]*?)(?:\n\n|$)/);
if (sourcesMatch) {
const sourcesText = sourcesMatch[1].trim();
const sourceLines = sourcesText.split('\n').filter((line: string) => line.trim());
sources = sourceLines.map((line: string) => {
const parts = line.split('|').map((p: string) => p.trim());
if (parts.length >= 2) {
return { title: parts[0], url: parts[1] };
}
// Fallback if format is different
return null;
}).filter((s: { title: string; url: string } | null): s is { title: string; url: string } => s !== null);
}
} catch (parseError) {
logger.error("📝 [API] Failed to parse delimited response:", parseError);
content = text || "Failed to generate content from the provided URLs.";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Ensure sources are populated with the original URLs if missing
if (sources.length === 0) {
sources = urls.map(url => {
try {
const hostname = new URL(url).hostname;
return { title: hostname, url };
} catch {
return { title: url, url };
}
});
}

// Create the note using workspace worker
const workerResult = await workspaceWorker("create", {
workspaceId,
title,
content,
sources,
folderId,
});

if (!workerResult.success) {
return new Response(JSON.stringify({ error: workerResult.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}

logger.info("📝 [API] Note created from URLs successfully:", { itemId: workerResult.itemId });

return new Response(JSON.stringify(workerResult), {
status: 200,
headers: { "Content-Type": "application/json" },
});

} catch (error) {
logger.error("❌ [API] Error creating note from URLs:", error);
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
Loading
Loading