diff --git a/baseai/memory/community-forum/data/forum-topics.json b/baseai/memory/community-forum/data/forum-topics.json new file mode 100644 index 000000000..68afc4368 --- /dev/null +++ b/baseai/memory/community-forum/data/forum-topics.json @@ -0,0 +1,7 @@ +[ + { + "url": "https://community.sourcegraph.com/t/updating-cody-search-index-hangs-forever/2275", + "title": "Troubleshooting updating cody search index... hangs forever", + "answer": "I have been on this for two weeks. Now, I believe it is not directly related to Cody. I had Python Extension deactivated, and activating it fixed the problem." + } +] \ No newline at end of file diff --git a/baseai/memory/community-forum/index.ts b/baseai/memory/community-forum/index.ts new file mode 100644 index 000000000..dbd080269 --- /dev/null +++ b/baseai/memory/community-forum/index.ts @@ -0,0 +1,42 @@ +import {MemoryI} from '@baseai/core'; +const memoryForum = (): MemoryI => ({ + name: 'community-forum', + description: + 'Solved questions and answers from the Sourcegraph community forum', + documents: { + meta: doc => { + // For a single JSON file containing all topics + // We'll extract metadata from the content + try { + // The content might be a JSON string with all topics + const topics = JSON.parse(doc.content); + + // Return basic metadata about this collection + // Convert count to string to satisfy the Record type + return { + url: 'https://community.sourcegraph.com/', + name: 'Sourcegraph Community Forum - Solved Topics', + count: String(topics.length) // Convert to string + }; + } catch (e) { + // If parsing fails, return basic info + // Add count property with a default value to match the structure + return { + url: 'https://community.sourcegraph.com/', + name: doc.name, + count: '0' + }; + } + } + }, + // Track the single JSON file + git: { + enabled: true, + include: ['baseai/memory/community-forum/data/forum-topics.json'], + gitignore: true, + deployedAt: '', + embeddedAt: '' + } +}); + +export default memoryForum; diff --git a/src/app/api/add-forum-post/route.ts b/src/app/api/add-forum-post/route.ts new file mode 100644 index 000000000..2ba64ef81 --- /dev/null +++ b/src/app/api/add-forum-post/route.ts @@ -0,0 +1,185 @@ +import {NextRequest, NextResponse} from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +// Helper function to convert HTML to plain text +function htmlToText(html: string): string { + // Remove HTML tags + let text = html.replace(/<[^>]*>/g, ' '); + + // Replace HTML entities + text = text + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' '); + + // Replace multiple spaces with a single space + text = text.replace(/\s+/g, ' '); + + // Trim leading and trailing spaces + return text.trim(); +} + +export async function POST(request: NextRequest) { + try { + const {url} = await request.json(); + + if (!url) { + return NextResponse.json({error: 'URL is required'}, {status: 400}); + } + + // Extract the topic ID from the URL + let topicUrl = url; + + // Ensure the URL ends with .json + if (!topicUrl.endsWith('.json')) { + topicUrl = `${topicUrl}.json`; + } + + // Fetch the topic data + const response = await fetch(topicUrl); + + if (!response.ok) { + return NextResponse.json( + { + error: `Failed to fetch topic: ${response.status} ${response.statusText}` + }, + {status: 500} + ); + } + + const topicData = await response.json(); + + if ( + !topicData.post_stream || + !topicData.post_stream.posts || + topicData.post_stream.posts.length === 0 + ) { + return NextResponse.json( + {error: 'Invalid topic data format'}, + {status: 400} + ); + } + + const posts = topicData.post_stream.posts; + + // Find the accepted answer + let solution = null; + + // First check if there's an accepted_answer object at the root level with excerpt + if (topicData.accepted_answer && topicData.accepted_answer.excerpt) { + // Use the excerpt directly as it's already in plain text + solution = htmlToText(topicData.accepted_answer.excerpt); + + } else { + // If no excerpt, try to find the post with the matching post_number + let solutionHtml = null; + + if ( + topicData.accepted_answer && + topicData.accepted_answer.post_number + ) { + const acceptedPost = posts.find( + (post: { post_number: number }) => + post.post_number === + topicData.accepted_answer.post_number + ); + if (acceptedPost) { + solutionHtml = acceptedPost.cooked; + } + } + + // If still no solution, try the post.accepted_answer property + if (!solutionHtml) { + const acceptedAnswerPost = posts.find( + (post: { accepted_answer: boolean }) => post.accepted_answer === true + ); + + if (acceptedAnswerPost) { + solutionHtml = acceptedAnswerPost.cooked; + } else if (posts.length > 1) { + // If no post is marked as accepted_answer, use the first reply as a fallback + solutionHtml = posts[1].cooked; + } + } + + if (solutionHtml) { + solution = htmlToText(solutionHtml); + } + } + + if (!solution) { + return NextResponse.json( + { + error: 'Could not extract solution from the topic' + }, + {status: 400} + ); + } + + // Extract the original URL without .json + const originalUrl = url.endsWith('.json') + ? url.substring(0, url.length - 5) + : url; + + // Create a more descriptive title + const descriptiveTitle = topicData.title + ? `Troubleshooting ${topicData.title.toLowerCase()}` + : posts[0].topic_slug.replace(/-/g, ' '); + + // Create the new entry with the target structure + const newEntry = { + url: originalUrl, + title: descriptiveTitle, + answer: solution + }; + + // Read the existing data + const dataFilePath = path.join( + process.cwd(), + 'baseai/memory/community-forum/data/forum-topics.json' + ); + let existingData = []; + + try { + const fileContent = fs.readFileSync(dataFilePath, 'utf8'); + existingData = JSON.parse(fileContent); + } catch (error) { + console.error('Error reading forum-topics.json:', error); + // If file doesn't exist or is empty, we'll start with an empty array + } + + // Check if the topic already exists (by URL) + const existingIndex = existingData.findIndex( + (item: { url: string }) => item.url === newEntry.url + ); + + if (existingIndex !== -1) { + // Update existing entry + existingData[existingIndex] = newEntry; + } else { + // Add new entry + existingData.push(newEntry); + } + + // Write the updated data back to the file + fs.writeFileSync(dataFilePath, JSON.stringify(existingData, null, 2)); + + return NextResponse.json({ + success: true, + message: 'Forum post added successfully', + entry: newEntry + }); + } catch (error) { + console.error('Error processing forum post:', error); + return NextResponse.json( + { + error: 'Failed to process forum post' + }, + {status: 500} + ); + } +} diff --git a/src/app/community-forum/page.tsx b/src/app/community-forum/page.tsx new file mode 100644 index 000000000..30d2d748e --- /dev/null +++ b/src/app/community-forum/page.tsx @@ -0,0 +1,97 @@ +'use client' + +import { useState } from 'react' +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +export default function AddForumPost() { + const [url, setUrl] = useState('') + const [status, setStatus] = useState<{ + type: 'idle' | 'loading' | 'success' | 'error', + message: string + }>({ type: 'idle', message: '' }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!url) { + setStatus({ + type: 'error', + message: 'Please enter a forum post URL' + }) + return + } + + setStatus({ type: 'loading', message: 'Processing forum post...' }) + + try { + const basePath = process.env.VERCEL_ENV === 'production' ? '/docs' : '' + const response = await fetch(`${basePath}/api/add-forum-post`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to add forum post') + } + + setStatus({ + type: 'success', + message: 'Forum post added successfully!' + }) + setUrl('') + } catch (error) { + console.error('Error adding forum post:', error) + setStatus({ + type: 'error', + message: error instanceof Error ? error.message : 'An unknown error occurred' + }) + } + } + + return ( +
+

Add Community Forum Post

+ +
+
+
+

+ Enter the URL of a Sourcegraph community forum post. +

+ +
+ setUrl(e.target.value)} + /> + +
+
+ + {status.message && ( +
+ {status.message} +
+ )} +
+
+
+ ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index b0d3ac8bd..34927f0af 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -103,6 +103,7 @@ function Header() { export function Layout({ children }: { children: React.ReactNode }) { let pathname = usePathname(); let isHomePage = pathname === '/'; + let isCommunityForumPage = pathname === '/community-forum'; return (
@@ -112,13 +113,14 @@ export function Layout({ children }: { children: React.ReactNode }) { {/* {isHomePage && } */}
-
-
-
-
- -
-
+ {!isCommunityForumPage && +
+
+
+
+ +
+
} {children}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 000000000..d43e9b600 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes { } + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/middleware.ts b/src/middleware.ts index 9c237a9e8..dc0ed19cd 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -137,5 +137,5 @@ export function middleware(request: NextRequest) { } export const config = { - matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'] + matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)', '/community-forum'] };