Skip to content
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

Feat/#218 add ai generation to markdown cells #228

Closed
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
32 changes: 25 additions & 7 deletions packages/api/ai/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type CodeCellType,
randomid,
type CellWithPlaceholderType,
type MarkdownCellType,
} from '@srcbook/shared';
import { type SessionType } from '../types.mjs';
import { readFileSync } from 'node:fs';
Expand Down Expand Up @@ -85,21 +86,34 @@ const makeGenerateCellEditSystemPrompt = (language: CodeLanguageType) => {
const makeGenerateCellEditUserPrompt = (
query: string,
session: SessionType,
cell: CodeCellType,
cell: CodeCellType | MarkdownCellType,
) => {
const cellLanguage = cell.type === 'markdown' ? 'markdown' : session.language;
const filteredCells =
cellLanguage === 'markdown'
? session.cells.filter((cell) => !(cell.type === 'package.json'))
: session.cells;

// Intentionally not passing in tsconfig.json here as that doesn't need to be in the prompt.

const inlineSrcbook = encode(
{ cells: session.cells, language: session.language },
{ cells: filteredCells, language: cellLanguage as CodeLanguageType },
{ inline: true },
);

const prompt = `==== BEGIN SRCBOOK ====
${inlineSrcbook}
==== END SRCBOOK ====

==== BEGIN CODE CELL ====
${
cell.type === 'code'
? `==== BEGIN CODE CELL ====
${cell.source}
==== END CODE CELL ====
==== END CODE CELL ====`
: `==== BEGIN MARKDOWN CELL ====
${cell.text}
==== END MARKDOWN CELL ====`
}

==== BEGIN USER REQUEST ====
${query}
Expand Down Expand Up @@ -180,10 +194,14 @@ export async function generateCells(
}
}

export async function generateCellEdit(query: string, session: SessionType, cell: CodeCellType) {
export async function generateCellEdit(
query: string,
session: SessionType,
cell: CodeCellType | MarkdownCellType,
) {
const model = await getModel();

const systemPrompt = makeGenerateCellEditSystemPrompt(session.language);
const cellLanguage = cell.type === 'markdown' ? 'markdown' : session.language;
const systemPrompt = makeGenerateCellEditSystemPrompt(cellLanguage as CodeLanguageType);
const userPrompt = makeGenerateCellEditUserPrompt(query, session, cell);
const result = await generateText({
model,
Expand Down
85 changes: 85 additions & 0 deletions packages/api/prompts/code-updater-markdown.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!-- srcbook:{"language":"markdown"} -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this in the prompt


## Instructions Context

You are tasked with editing a **Markdown cell** in a Srcbook.

A Srcbook is a **Markdown-compatible notebook**, used for documentation or text-based content.

### Srcbook Spec

The structure of a Srcbook:
0. The language comment: `<!-- srcbook:{"language":"markdown"} -->`
1. Title cell (heading 1)
2. N more cells, which are either:
- **Markdown cells** (GitHub flavored Markdown)
- Markdown cells, which have a filename and source content.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, should be code cells



# Important Note:
Markdown cells cannot use h1 or h6 headings, as these are reserved for Srcbook. **Do not use h1 (#) or h6 (######) headings in the content.**

The user is already working on an existing Srcbook and is asking you to edit a specific Markdown cell.
The Srcbook contents will be passed to you as context, as well as the user's request about the intended edits for the Markdown cell.

---

## Example Srcbook

<!-- srcbook:{"language":"markdown"} -->

### Getting Started

#### What are Srcbooks?

Srcbooks are an interactive way of organizing and presenting information. They are similar to other notebooks but unique in their flexibility and format.

#### Dependencies

You can include any necessary information, resources, or links to external content.

##### Introduction

This is a Markdown cell showcasing various Markdown features.

#### Features Overview

##### Text Formatting

- **Bold text**
- *Italic text*
- ~~Strikethrough text~~

##### Lists

- **Unordered List:**
- Item 1
- Item 2

- **Ordered List:**
1. First item
2. Second item

##### Code Blocks

Inline code: `console.log("Hello, Markdown!")`

##### Links

[Click here to visit Google](https://www.google.com)

##### Images

![Alt text](image.png)

---

## Final Instructions

The user's Srcbook will be passed to you, surrounded with `==== BEGIN SRCBOOK ====` and `==== END SRCBOOK ====`.
The specific **Markdown cell** they want updated will also be passed to you, surrounded with `==== BEGIN MARKDOWN CELL ====` and `==== END MARKDOWN CELL ====`.
The user's intent will be passed to you between `==== BEGIN USER REQUEST ====` and `==== END USER REQUEST ====`.

Your job is to edit the cell based on the contents of the Srcbook and the user's intent.
Act as a **Markdown expert**, writing the best possible content you can. Focus on being **elegant, concise, and clear**.
**ONLY RETURN THE MARKDOWN TEXT , NO PREAMBULE, NO SUFFIX, NO CODE FENCES (LIKE TRIPLE BACKTICKS) ONLY THE MARKDOWN**.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PREAMBULE should be PREAMBLE

5 changes: 3 additions & 2 deletions packages/api/srcmd/encoding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export function encode(srcbook: SrcbookWithPlacebolderType, options: { inline: b
const encoded = [
encodeMetdata(srcbook),
encodeTitleCell(titleCell),
encodePackageJsonCell(packageJsonCell, options),
...((srcbook.language as string) !== 'markdown'
? [encodePackageJsonCell(packageJsonCell, options)]
: []),
...cells.map((cell) => {
switch (cell.type) {
case 'code':
Expand All @@ -34,7 +36,6 @@ export function encode(srcbook: SrcbookWithPlacebolderType, options: { inline: b
}
}),
];

// End every file with exactly one newline.
return encoded.join('\n\n').trimEnd() + '\n';
}
Expand Down
53 changes: 53 additions & 0 deletions packages/web/src/components/ai-prompt-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Sparkles, MessageCircleWarning, X } from 'lucide-react';
import TextareaAutosize from 'react-textarea-autosize';
import { Button } from '@/components/ui/button';
import AiGenerateTipsDialog from '@/components/ai-generate-tips-dialog';
import { useNavigate } from 'react-router-dom';

interface AiPromptInputProps {
prompt: string;
setPrompt: (prompt: string) => void;
onClose: () => void;
aiEnabled: boolean;
}

export function AiPromptInput({ prompt, setPrompt, onClose, aiEnabled }: AiPromptInputProps) {
const navigate = useNavigate();
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-start justify-between px-1">
<div className="flex items-start flex-grow">
<Sparkles size={16} className="m-2.5" />
<TextareaAutosize
className="flex w-full rounded-sm bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none resize-none"
placeholder="Ask the AI to edit this cell..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</div>
<div className="flex items-center gap-1">
<AiGenerateTipsDialog>
<Button size="icon" variant="icon">
<MessageCircleWarning size={16} />
</Button>
</AiGenerateTipsDialog>
<Button size="icon" variant="icon" onClick={onClose}>
<X size={16} />
</Button>
</div>
</div>
{!aiEnabled && (
<div className="flex items-center justify-between bg-warning text-warning-foreground rounded-sm text-sm px-3 py-1 m-3">
<p>AI provider not configured.</p>
<button
className="font-medium underline cursor-pointer"
onClick={() => navigate('/settings')}
aria-hidden="true"
>
Settings
</button>
</div>
)}
</div>
);
}
69 changes: 10 additions & 59 deletions packages/web/src/components/cells/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,13 @@ import { useEffect, useRef, useState } from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import Shortcut from '@/components/keyboard-shortcut';
import { useNavigate } from 'react-router-dom';

import { useHotkeys } from 'react-hotkeys-hook';
import CodeMirror, { keymap, Prec } from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import {
Info,
Play,
Trash2,
Sparkles,
X,
MessageCircleWarning,
LoaderCircle,
Maximize,
Minimize,
} from 'lucide-react';
import TextareaAutosize from 'react-textarea-autosize';
import AiGenerateTipsDialog from '@/components/ai-generate-tips-dialog';
import { Info, Play, Trash2, Sparkles, LoaderCircle, Maximize, Minimize } from 'lucide-react';

import {
CellType,
CodeCellType,
Expand All @@ -46,6 +35,7 @@ import { EditorView } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { unifiedMergeView } from '@codemirror/merge';
import { type Diagnostic, linter } from '@codemirror/lint';
import { AiPromptInput } from '@/components/ai-prompt-input';
import { tsHover } from './hover';
import { mapTsServerLocationToCM } from './util';
import { toast } from 'sonner';
Expand Down Expand Up @@ -395,7 +385,6 @@ function Header(props: {
} = props;

const { aiEnabled } = useSettings();
const navigate = useNavigate();

return (
<>
Expand Down Expand Up @@ -554,50 +543,12 @@ function Header(props: {
</div>
</div>
{['prompting', 'generating'].includes(cellMode) && (
<div className="flex flex-col gap-1.5">
<div className="flex items-start justify-between px-1">
<div className="flex items-start flex-grow">
<Sparkles size={16} className="m-2.5" />
<TextareaAutosize
className="flex w-full rounded-sm bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none resize-none"
// eslint-disable-next-line jsx-a11y/no-autofocus -- needed for action flow, should not limit accessibility
autoFocus
placeholder="Ask the AI to edit this cell..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</div>
<div className="flex items-center gap-1">
<AiGenerateTipsDialog>
<Button size="icon" variant="icon">
<MessageCircleWarning size={16} />
</Button>
</AiGenerateTipsDialog>
<Button
size="icon"
variant="icon"
onClick={() => {
setCellMode('off');
setPrompt('');
}}
>
<X size={16} />
</Button>
</div>
</div>

{!aiEnabled && (
<div className="flex items-center justify-between bg-warning text-warning-foreground rounded-sm text-sm px-3 py-1 m-3">
<p>AI provider not configured.</p>
<button
className="font-medium underline cursor-pointer"
onClick={() => navigate('/settings')}
>
Settings
</button>
</div>
)}
</div>
<AiPromptInput
prompt={prompt}
setPrompt={setPrompt}
onClose={() => setCellMode('off')}
aiEnabled={aiEnabled}
/>
)}
</>
);
Expand Down
Loading