Skip to content

Community form #1074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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
7 changes: 7 additions & 0 deletions baseai/memory/community-forum/data/forum-topics.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
42 changes: 42 additions & 0 deletions baseai/memory/community-forum/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> 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;
185 changes: 185 additions & 0 deletions src/app/api/add-forum-post/route.ts
Original file line number Diff line number Diff line change
@@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&nbsp;/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}
);
}
}
97 changes: 97 additions & 0 deletions src/app/community-forum/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto py-12 max-w-3xl">
<h1 className="text-[2.25rem] font-normal mb-8 text-[#0f172A] dark:text-white">Add Community Forum Post</h1>

<div className="bg-card p-6 rounded-lg shadow-sm">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<p className="text-sm text-muted-foreground mb-4">
Enter the URL of a Sourcegraph community forum post.
</p>

<div className="flex w-full items-center space-x-3">
<Input
id="url"
type="text"
placeholder="Enter Post URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Button
type="submit"
disabled={status.type === 'loading'}
>
{status.type === 'loading' ? 'Processing...' : 'Add Post'}
</Button>
</div>
</div>

{status.message && (
<div className={`p-4 rounded-md ${status.type === 'success' ? 'bg-green-50 text-green-700' :
status.type === 'error' ? 'bg-red-50 text-red-700' :
'text-vermilion-11'
}`}>
{status.message}
</div>
)}
</form>
</div>
</div>
)
}
16 changes: 9 additions & 7 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex w-full flex-col">
Expand All @@ -112,13 +113,14 @@ export function Layout({ children }: { children: React.ReactNode }) {
{/* {isHomePage && <DemoLayout />} */}

<div className="relative mx-auto flex w-full max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div className="hidden lg:relative lg:block lg:flex-none">
<div className="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-transparent dark:block" />
<div className="absolute bottom-0 right-0 top-28 hidden w-px bg-transparent dark:block" />
<div className="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
<Navigation />
</div>
</div>
{!isCommunityForumPage &&
<div className="hidden lg:relative lg:block lg:flex-none">
<div className="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-transparent dark:block" />
<div className="absolute bottom-0 right-0 top-28 hidden w-px bg-transparent dark:block" />
<div className="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
<Navigation />
</div>
</div>}
{children}
</div>
</div>
Expand Down
24 changes: 24 additions & 0 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"

export { Input }
2 changes: 1 addition & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
};