# Project export: Skribez AI Writing Assistant

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: AI-powered writing assistant that helps students think, write, and improve -without doing the work for them!
- Devpost: https://devpost.com/software/skribez-ai-writing-assistant
- GitHub: https://github.com/danila6231/Scribez
- Demo: https://www.skribez.com/
- Team: 4 GitHub contributor(s) — Dani (36 commits), Hai (26 commits), Noah Weis (20 commits), Lorenzo Bolls (18 commits)

## Devpost submission (written by the team)

### Inspiration

As students ourselves, we noticed a gap: current AI tools either write for you or leave you hanging. We wanted something in between - an assistant that helps you think, draft, and revise, but doesn’t take over. So we built Skribez: an AI-enhanced writing tool that supports student agency.

### What it does

Skribez is an AI-enhanced writing environment built for students. It has two main components: Text Editor (left side): A clean, markdown-friendly word processor with formatting tools and image support. AI Assistant (right side): A dual-mode assistant that supports writing in two ways: Ask Mode: For conceptual questions, quick lookups, and research help. Edit Mode: For in-context rewriting, edits, and exploratory drafting - inspired by developer tools like Cursor, but tailored for writing. The goal is to encourage autonomous writing while giving just enough smart support to help students explore, refine, and defend their ideas.

### How we built it

Frontend: React + Lexical for a flexible, modern writing experience. Backend: FastAPI connected to two AI models via Anthropic APIs: Groq: Lightweight, fast, and ideal for answering simple questions or making small edits. Claude Opus: A heavier, more powerful model used for complex reasoning, nuanced edits, and deep research tasks. We built dynamic routing logic between the two, so the assistant knows when to escalate from Groq to Opus for better support - giving the best of both speed and intelligence.

### Challenges we ran into

Building a clean yet powerful dual-mode AI assistant took some thought - especially around UX clarity. Getting Grok and Opus to play nicely together took tweaking, especially on fallback and redirect logic. Tuning the edit mode to feel natural without taking control from the writer required some iteration. Building our own text editor that supports edits from both people and AI agents

### Accomplishments we're proud of

Created a truly agentic workflow for writers - not just an AI chatbot glued onto an editor. Leveraged Groq’s speed to deliver fast, seamless interactions while still enabling complex tasks with Opus. Built a tool that satisfies the need of a large userbase.

### What we learned

Students don’t want full essays - they want someone smart to bounce ideas off and help polish their own. Groq, while lightweight, is surprisingly capable when used intentionally and paired with the right fallback strategy.

### What's next

Media Management: Inserting and editing graphs, videos, and more dynamic content inside essays. Assignment Integration: Connect with platforms like Canvas via API to allow students to work directly on their coursework. Reaching out to universities for collaboration. More Personalization: Let users adjust how much help they want from AI and in what form — hints, edits, questions, etc.

## README (from the GitHub repository)

# Writing Tool for Students

An AI-enhanced writing interface that supports students throughout the writing process without taking away their agency.

## Features

- **Text Editor (Left Side)**: A standard word processor with typical formatting tools, markdown support, and image insertion
- **AI Chat Assistant (Right Side)**: Interactive chat window for brainstorming ideas, getting writing suggestions, and receiving feedback

## Tech Stack

- **Frontend**: React with Vite
  - Text Editor: Lexical (Meta's extensible text editor framework)
  - Styling: CSS
- **Backend**: FastAPI (Python)

## Project Structure

```
├── frontend/           # React frontend application
│   ├── public/        # Static assets
│   │   ├── components/  # React components
│   │   │   ├── Editor/  # Text editor components
│   │   │   ├── Chat/    # Chat interface components
│   │   │   └── Layout.jsx
│   │   ├── styles/      # CSS files
│   │   ├── App.jsx      # Main app component
│   │   └── main.jsx     # Entry point
│   └── package.json     # Frontend dependencies
│
├── backend/           # FastAPI backend
│   ├── app/          # Application code
│   │   ├── main.py   # FastAPI app
│   │   └── routers/  # API routes
│   └── requirements.txt
│
└── README.md
```

## Getting Started

### Backend Setup

1. Navigate to the backend directory:
   ```bash
   cd backend
   ```

2. Create a virtual environment:
   ```bash
   python -m venv venv
   source venv/bin/activate  # On Windows: venv\Scripts\activate
   ```

3. Install dependencies:
   ```bash
   pip install -r requirements.txt
   ```

4. Run the backend server:
   ```bash
   uvicorn app.main:app --reload
   ```

### Frontend Setup

1. Navigate to the frontend directory:
   ```bash
   cd frontend
   ```

2. Install dependencies:
   ```bash
   npm install
   ```

3. Run the development server:
   ```bash
   npm run dev
   ```

The application will be available at `http://localhost:5173`

## Development Notes

- This is a starting version with placeholder AI functionality
- The text editor uses Lexical for rich text editing capabilities
- The chat interface is ready for AI integration but currently returns placeholder responses

## Detected evidence (automated analysis)

Indexed codebase: 51 recognized source files, 297 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- MongoDB (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (57 of 57)

```
.gitignore
backend/API_SETUP.md
backend/app/__init__.py
backend/app/cloudinary_config.py
backend/app/database.py
backend/app/diff.py
backend/app/llm.py
backend/app/main.py
backend/app/models.py
backend/app/routers/__init__.py
backend/app/routers/chat.py
backend/app/routers/diff.py
backend/app/routers/documents.py
backend/app/routers/images.py
backend/MONGODB_SETUP.md
backend/README.md
backend/requirements.txt
backend/test_api.py
backend/test_llm.py
backend/test_mongo.py
frontend/.gitignore
frontend/index.html
frontend/package.json
frontend/src/App.jsx
frontend/src/assets/README.md
frontend/src/components/Chat/ChatMessage.jsx
frontend/src/components/Chat/ChatWindow.jsx
frontend/src/components/Dashboard/Dashboard.jsx
frontend/src/components/Dashboard/DocumentGroup.jsx
frontend/src/components/Dashboard/DocumentItem.jsx
frontend/src/components/Diff/diff.css
frontend/src/components/Diff/diffTester.css
frontend/src/components/Diff/DiffTester.jsx
frontend/src/components/Diff/DiffView.jsx
frontend/src/components/Diff/LexicalDiffPlugin.jsx
frontend/src/components/Editor/CommandKModal.jsx
frontend/src/components/Editor/Editor.jsx
frontend/src/components/Editor/editorConfig.js
frontend/src/components/Editor/Toolbar.jsx
frontend/src/components/Landing/LandingPage.jsx
frontend/src/components/Layout.jsx
frontend/src/components/Print/PrintView.jsx
frontend/src/config/api.js
frontend/src/main.jsx
frontend/src/services/api.js
frontend/src/styles/dashboard.css
frontend/src/styles/editor.css
frontend/src/styles/main.css
frontend/src/styles/print.css
frontend/src/utils/apiTest.js
frontend/src/utils/debugAPI.js
frontend/src/utils/testDiffApplication.js
frontend/vercel.json
frontend/vite.config.js
package.json
README.md
test_diff_acceptance.md
```

### Dependencies

- backend/requirements.txt: anthropic@==0.28.0, certifi@==2024.8.30, cloudinary@==1.44.1, dnspython@==2.7.0, fastapi@==0.104.1, google-generativeai@==0.3.2, groq@==0.9.0, httpx@==0.25.2, motor@==3.3.2, pydantic@==2.5.0, pymongo[srv]@==4.6.1, pyOpenSSL@==24.0.0, python-dotenv@==1.0.0, python-multipart@==0.0.6, sse-starlette@==1.6.5, uvicorn[standard]@==0.24.0
- frontend/package.json: @clerk/clerk-react@^5.32.0, @lexical/code@^0.12.2, @lexical/file@^0.12.2, @lexical/link@^0.12.2, @lexical/list@^0.12.2, @lexical/markdown@^0.12.6, @lexical/react@^0.12.2, @lexical/rich-text@^0.12.2, @lexical/selection@^0.12.2, @lexical/table@^0.12.2, @lexical/utils@^0.12.2, @types/react@^18.2.43, @types/react-dom@^18.2.17, @vitejs/plugin-react@^4.2.1, axios@^1.6.2, eslint@^8.55.0, eslint-plugin-react@^7.33.2, eslint-plugin-react-hooks@^4.6.0, eslint-plugin-react-refresh@^0.4.5, lexical@^0.12.2, mermaid@^11.7.0, react@^18.2.0, react-dom@^18.2.0, react-markdown@^10.1.0, react-router-dom@^7.6.2, rehype-highlight@^7.0.2, rehype-raw@^7.0.0, remark-gfm@^4.0.1, vite@^5.4.19
- package.json: @clerk/clerk-react@^5.32.0

### Recent commits (newest first)

- Updated Website Icon
- Merge branch 'main' of https://github.com/danila6231/aiberkeley-hack
- Revert "Revert "you can now scroll the diff""
- Revert "you can now scroll the diff"
- Changed signin button color
- Merge branch 'main' of https://github.com/danila6231/aiberkeley-hack
- Update Mermaid integration: add Mermaid diagram support in the editor with loading and error handling, and include responsive styles for the toolbar. Also, update Claude model name in configuration.
- balls
- slight dashboard UI changes
- done please
- changed the ui colors
- changing dashboard UI and logo
- changed some colors
- changed colors in editor
- Merge branch 'main' of https://github.com/danila6231/aiberkeley-hack
- Update Claude model name and add Mermaid diagram generation endpoint with request/response models in chat router.
- you can now scroll the diff
- Merge branch 'main' of https://github.com/danila6231/aiberkeley-hack
- removed scroll bar from your mom
- Increase max_tokens for Groq and Claude responses to enhance output capacity and improve response quality.

## Key source files (fetched from GitHub, selected and truncated for size)

### test_diff_acceptance.md

```markdown
# Test Plan for Diff Acceptance Functionality

## Overview
This document outlines the test plan for verifying that both individual change acceptance and "accept all" functionality work correctly in the diff view.

## Setup
1. Backend server running on http://localhost:8000
2. Frontend server running on http://localhost:5173
3. Browser console open for debugging

## Test Cases

### Test 1: Accept All Changes
1. Create a new document with text: "The quick brown fox jumps over the lazy dog."
2. Switch to "Edit" mode
3. Request: "Change quick to slow, jumps to leaps, and dog to cat"
4. When diff view appears:
   - Verify 3 changes are shown
   - Click "Accept All"
   - Verify editor updates with all changes applied
   - Expected result: "The slow brown fox leaps over the lazy cat."

### Test 2: Individual Change Acceptance
1. Create a document with text: "The quick brown fox jumps over the lazy dog."
2. Switch to "Edit" mode
3. Request: "Change quick to slow, jumps to leaps, and dog to cat"
4. When diff view appears:
   - Accept only the first change (quick → slow)
   - Verify editor updates immediately
   - Accept the third change (dog → cat)
   - Verify editor updates with both changes
   - Expected result: "The slow brown fox jumps over the lazy cat."

### Test 3: Mixed Accept/Reject
1. Create a document with text: "Hello world. This is a test document."
2. Switch to "Edit" mode
3. Request: "Change Hello to Hi, world to there, and test to sample"
4. When diff view appears:
   - Accept first change (Hello → Hi)
   - Reject second change (world → there)
   - Accept third change (test → sample)
   - Expected result: "Hi world. This is a sample document."

### Test 4: Reject All Changes
1. Create any document
2. Request an edit
3. Click "Reject All"
4. Verify document remains unchanged

### Test 5: Close Without Saving
1. Create any document
2. Request an edit
3. Accept some changes
4. Click X or click outside the diff overlay
5. Verify document returns to original state

## Debug Commands
Run these in the browser console:

```javascript
// Test the diff application logic
window.testDiffApplication()

// Check current editor content
window.lexicalEditor.getEditorState().read(() => {
  const root = $getRoot();
  console.log('Editor content:', root.getTextContent());
});

// Check diff changes
console.log('Current diff changes:', window.diffChanges);
```

## Expected Issues Fixed
1. ✅ Individual change acceptance should now update the editor immediately
2. ✅ Accept all should apply all changes correctly
3. ✅ Rejected changes should not be applied
4. ✅ Closing the diff view should restore original content
5. ✅ State should be properly tracked for accepted/rejected changes 
```

### backend/MONGODB_SETUP.md

```markdown
# MongoDB Atlas Setup Guide

## 1. Create MongoDB Atlas Account
1. Go to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)
2. Sign up for a free account
3. Create a new cluster (free tier is sufficient)

## 2. Configure Database Access
1. Go to Database Access in the left sidebar
2. Add a new database user with username and password
3. Remember these credentials for the connection string

## 3. Configure Network Access
1. Go to Network Access in the left sidebar
2. Add your IP address or use 0.0.0.0/0 for development (not recommended for production)

## 4. Get Connection String
1. Go to your cluster and click "Connect"
2. Choose "Connect your application"
3. Copy the connection string

## 5. Configure Environment Variables
Create a `.env` file in the backend directory:

```bash
# MongoDB Configuration
MONGODB_URL=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/?retryWrites=true&w=majority
DATABASE_NAME=writing_tool

# Existing LLM API Keys
GROQ_API_KEY=your_groq_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
GOOGLE_API_KEY=your_google_api_key_here
```

Replace `<username>`, `<password>`, and `<cluster>` with your actual values.

## 6. Test the Connection
Run the test script to verify everything is working:

```bash
python test_api.py
``` 
```

### package.json

```
{
  "dependencies": {
    "@clerk/clerk-react": "^5.32.0"
  }
}

```

### backend/requirements.txt

```
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-dotenv==1.0.0
pydantic==2.5.0
python-multipart==0.0.6

# LLM Libraries
groq==0.9.0
anthropic==0.28.0
google-generativeai==0.3.2

# Database
motor==3.3.2
pymongo[srv]==4.6.1
dnspython==2.7.0
certifi==2024.8.30
pyOpenSSL==24.0.0

httpx==0.25.2
sse-starlette==1.6.5
cloudinary==1.44.1 

```

### frontend/package.json

```
{
  "name": "writing-tool-frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@clerk/clerk-react": "^5.32.0",
    "@lexical/code": "^0.12.2",
    "@lexical/file": "^0.12.2",
    "@lexical/link": "^0.12.2",
    "@lexical/list": "^0.12.2",
    "@lexical/markdown": "^0.12.6",
    "@lexical/react": "^0.12.2",
    "@lexical/rich-text": "^0.12.2",
    "@lexical/selection": "^0.12.2",
    "@lexical/table": "^0.12.2",
    "@lexical/utils": "^0.12.2",
    "axios": "^1.6.2",
    "lexical": "^0.12.2",
    "mermaid": "^11.7.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-markdown": "^10.1.0",
    "react-router-dom": "^7.6.2",
    "rehype-highlight": "^7.0.2",
    "rehype-raw": "^7.0.0",
    "remark-gfm": "^4.0.1"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "vite": "^5.4.19"
  }
}

```

### frontend/src/main.jsx

```javascript
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom';
import { ClerkProvider } from '@clerk/clerk-react';
import App from './App.jsx'
import './styles/main.css'
import './styles/editor.css'

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;

if (!PUBLISHABLE_KEY) {
  throw new Error("Missing Clerk Publishable Key");
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ClerkProvider 
      publishableKey={PUBLISHABLE_KEY}
      navigate={(to) => window.history.pushState(null, '', to)}
    >
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </ClerkProvider>
  </React.StrictMode>,
) 
```

### backend/app/main.py

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from app.routers import chat, diff, documents, images
from app.database import create_indexes

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    await create_indexes()
    yield
    # Shutdown

app = FastAPI(title="Writing Tool API", version="1.0.0", lifespan=lifespan)

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # In production, replace with specific origins
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers
app.include_router(chat.router, prefix="/api/chat", tags=["chat"])
app.include_router(documents.router, prefix="/api/documents", tags=["documents"])
app.include_router(diff.router, prefix="/api/diff", tags=["diff"])
app.include_router(images.router, prefix="/api/images", tags=["images"])

@app.get("/")
async def root():
    return {"message": "Writing Tool API is running"}

@app.get("/health")
async def health_check():
    return {"status": "healthy"} 
```

### frontend/src/App.jsx

```javascript
import React from 'react';
import Layout from './components/Layout';
import Dashboard from './components/Dashboard/Dashboard';
import DiffTester from './components/Diff/DiffTester';
import PrintView from './components/Print/PrintView';
import { Routes, Route, Link, Navigate } from 'react-router-dom';
import LandingPage from './components/Landing/LandingPage';
import { 
  SignedIn
} from '@clerk/clerk-react';
import { useAuth } from "@clerk/clerk-react";

function WordEditor() {
  return <Layout />;
}

// Protected Route Component
function ProtectedEditor() {
  return (
    <SignedIn>
      <WordEditor />
    </SignedIn>
  );
}
function ProtectedDashboard() {
  return (
    <SignedIn>
      <Dashboard />
    </SignedIn>
  );
}

// Protected Print Component
function ProtectedPrintView() {
  return (
    <SignedIn>
      <PrintView />
    </SignedIn>
  );
}

function userId(){
  const { isLoaded, isSignedIn, userId } = useAuth();

  // You have to wait for Clerk to load before you can check the status
  if (!isLoaded) {
    return <div>Loading...</div>;
  }

  if (!isSignedIn) {
    return <div>Please sign in to see your profile.</div>;
  }

  // Once loaded and signed in, you have the userId
  return <div>Your User ID is: {userId}</div>;
}
function App() {
  return (
    <Routes>
      <Route path="/" element={<LandingPage />} />
      <Route path="/editor" element={<Navigate to="/dashboard" replace />} />
      <Route path="/editor/:documentId" element={<ProtectedEditor />} />
      <Route path="/dashboard" element={<ProtectedDashboard />} />
      <Route path="/diff-tester" element={<DiffTester />} />
      <Route path="/print/:documentId" element={<ProtectedPrintView />} />
      {/* Catch-all route for invalid URLs */}
      <Route path="*" element={<Navigate to="/dashboard" replace />} />
    </Routes>
  );
}

export default App; 
```

### frontend/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/png" href="/favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Skribez</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html> 
```

### frontend/vite.config.js

```javascript
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  // Load env file based on `mode` in the current working directory.
  const env = loadEnv(mode, process.cwd(), '');
  
  // Use VITE_API_URL if defined, otherwise fallback to localhost:8000
  const apiUrl = env.VITE_API_URL || 'http://localhost:8000';
  
  return {
    plugins: [react()],
    server: {
      port: 5173,
      proxy: {
        '/api': {
          target: apiUrl,
          changeOrigin: true,
        }
      }
    }
  }
}) 
```

[41 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]