prettier formatting

This commit is contained in:
Daniel O'Connell
2023-10-03 23:51:52 +02:00
parent ac25ce7746
commit 23c01d45eb
17 changed files with 808 additions and 716 deletions
+10 -8
View File
@@ -1,12 +1,14 @@
import {useState} from 'react'
import {ShowCitation, CitationsBlock} from './citations'
import {GlossarySpan} from './glossary'
import type {Citation, AssistantEntry as AssistantType} from '../types'
import { useState } from "react";
import { ShowCitation, CitationsBlock } from "./citations";
import { GlossarySpan } from "./glossary";
import type { Citation, AssistantEntry as AssistantType } from "../types";
export const AssistantEntry: React.FC<{entry: AssistantType}> = ({entry}) => {
export const AssistantEntry: React.FC<{ entry: AssistantType }> = ({
entry,
}) => {
return (
<div className="mt-3 mb-8">
{entry.content.split('\n').map((paragraph, i) => (
{entry.content.split("\n").map((paragraph, i) => (
<CitationsBlock
key={i}
text={paragraph}
@@ -25,5 +27,5 @@ export const AssistantEntry: React.FC<{entry: AssistantType}> = ({entry}) => {
}
</ul>
</div>
)
}
);
};
+95 -84
View File
@@ -1,5 +1,5 @@
import {useState, useEffect} from 'react'
import {queryLLM, getStampyContent, runSearch} from '../hooks/useSearch'
import { useState, useEffect } from "react";
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
import type {
CurrentSearch,
@@ -8,102 +8,111 @@ import type {
AssistantEntry as AssistantEntryType,
LLMSettings,
Followup,
} from '../types'
import {SearchBox} from '../components/searchbox'
import {AssistantEntry} from '../components/assistant'
import {Entry as EntryTag} from '../components/entry'
} from "../types";
import { SearchBox } from "../components/searchbox";
import { AssistantEntry } from "../components/assistant";
import { Entry as EntryTag } from "../components/entry";
const MAX_FOLLOWUPS = 4
const MAX_FOLLOWUPS = 4;
type State =
| {
state: 'idle'
state: "idle";
}
| {
state: 'loading'
phase: 'semantic' | 'prompt' | 'llm'
citations: Citation[]
state: "loading";
phase: "semantic" | "prompt" | "llm";
citations: Citation[];
}
| {
state: 'streaming'
response: AssistantEntryType
}
state: "streaming";
response: AssistantEntryType;
};
// smooth-scroll to the bottom of the window if we're already less than 30% a screen away
// note: finicky interaction with "smooth" - maybe fix later.
function scroll30() {
if (document.documentElement.scrollHeight - window.scrollY > window.innerHeight * 1.3) return
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})
if (
document.documentElement.scrollHeight - window.scrollY >
window.innerHeight * 1.3
)
return;
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
}
type ChatParams = {
sessionId: string
settings: LLMSettings
onQuery?: (q: string) => any
onNewEntry?: (history: Entry[]) => any
}
sessionId: string;
settings: LLMSettings;
onQuery?: (q: string) => any;
onNewEntry?: (history: Entry[]) => any;
};
const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
const [entries, setEntries] = useState<Entry[]>([])
const [current, setCurrent] = useState<CurrentSearch>()
const [citations, setCitations] = useState<Citation[]>([])
const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
const [entries, setEntries] = useState<Entry[]>([]);
const [current, setCurrent] = useState<CurrentSearch>();
const [citations, setCitations] = useState<Citation[]>([]);
const updateCurrent = (current: CurrentSearch) => {
setCurrent(current)
if (current?.phase === 'streaming') {
scroll30()
setCurrent(current);
if (current?.phase === "streaming") {
scroll30();
}
}
};
const updateCitations = (allCitations: Citation[], current?: CurrentSearch) => {
if (!current) return
const updateCitations = (
allCitations: Citation[],
current?: CurrentSearch
) => {
if (!current) return;
const entryCitations = Array.from(current.citationsMap.values())
const entryCitations = Array.from(current.citationsMap.values());
if (!entryCitations.some((c) => !c.index)) {
// All of the entries citations have indexes, so there weren't any changes since the last check
return
return;
}
// Get a mapping of all known citations, so as to reuse them if they appear again
const citationsMapping = Object.fromEntries(allCitations.map((c) => [c.title + c.url, c.index]))
const citationsMapping = Object.fromEntries(
allCitations.map((c) => [c.title + c.url, c.index])
);
entryCitations.forEach((c) => {
const hash = c.title + c.url
const index = citationsMapping[hash]
const hash = c.title + c.url;
const index = citationsMapping[hash];
if (!index) {
c.index = allCitations.length + 1
allCitations.push(c)
c.index = allCitations.length + 1;
allCitations.push(c);
} else {
c.index = index
c.index = index;
}
})
setCitations(allCitations)
setCurrent(current)
}
});
setCitations(allCitations);
setCurrent(current);
};
const addEntry = (entry: Entry) => {
setEntries((prev) => {
const entries = [...prev, entry]
const entries = [...prev, entry];
if (onNewEntry) {
onNewEntry(entries)
onNewEntry(entries);
}
return entries
})
}
return entries;
});
};
const search = async (
query: string,
query_source: 'search' | 'followups',
query_source: "search" | "followups",
enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void,
controller: AbortController
) => {
// clear the query box, append to entries
const userEntry: Entry = {
role: 'user',
content: query_source === 'search' ? query : query.split('\n', 2)[1]!,
}
role: "user",
content: query_source === "search" ? query : query.split("\n", 2)[1]!,
};
const {result, followups} = await runSearch(
const { result, followups } = await runSearch(
query,
query_source,
settings,
@@ -111,44 +120,46 @@ const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
updateCurrent,
sessionId,
controller
)
if (result.content !== 'aborted') {
addEntry(userEntry)
addEntry(result)
enable(followups || [])
scroll30()
);
if (result.content !== "aborted") {
addEntry(userEntry);
addEntry(result);
enable(followups || []);
scroll30();
} else {
enable([])
enable([]);
}
setCurrent(undefined)
}
setCurrent(undefined);
};
var last_entry = <></>
var last_entry = <></>;
switch (current?.phase) {
case 'semantic':
last_entry = <p>Loading: Performing semantic search...</p>
break
case 'prompt':
last_entry = <p>Loading: Creating prompt...</p>
break
case 'llm':
last_entry = <p>Loading: Waiting for LLM...</p>
break
case 'streaming':
updateCitations(citations, current)
last_entry = <AssistantEntry entry={current} />
break
case 'followups':
case "semantic":
last_entry = <p>Loading: Performing semantic search...</p>;
break;
case "prompt":
last_entry = <p>Loading: Creating prompt...</p>;
break;
case "llm":
last_entry = <p>Loading: Waiting for LLM...</p>;
break;
case "streaming":
updateCitations(citations, current);
last_entry = <AssistantEntry entry={current} />;
break;
case "followups":
last_entry = (
<>
<AssistantEntry entry={current} />
<p>Checking for followups...</p>
</>
)
break
);
break;
default:
last_entry = <button onClick={() => setEntries([])}>Clear history</button>
break
last_entry = (
<button onClick={() => setEntries([])}>Clear history</button>
);
break;
}
return (
@@ -160,7 +171,7 @@ const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
{last_entry}
</ul>
)
}
);
};
export default Chat
export default Chat;
+63 -52
View File
@@ -1,5 +1,5 @@
import type {Citation} from '../types'
import {Colours, A} from './html'
import type { Citation } from "../types";
import { Colours, A } from "./html";
export const formatCitations: (text: string) => string = (text) => {
// ---------------------- normalize citation form ----------------------
@@ -13,10 +13,10 @@ export const formatCitations: (text: string) => string = (text) => {
(block: string) =>
block
.split(',')
.split(",")
.map((x) => x.trim())
.join('][')
)
.join("][")
);
// transform all things that look like [(a), (b), (c)] into [(a)][(b)][(c)]
response = response.replace(
@@ -24,107 +24,118 @@ export const formatCitations: (text: string) => string = (text) => {
(block: string) =>
block
.split(',')
.split(",")
.map((x) => x.trim())
.join('][')
)
.join("][")
);
// transform all things that look like [(a)] into [a]
response = response.replace(/\[\(([a-z]+)\)\]/g, (_match: string, x: string) => `[${x}]`)
response = response.replace(
/\[\(([a-z]+)\)\]/g,
(_match: string, x: string) => `[${x}]`
);
// transform all things that look like [ a ] into [a]
response = response.replace(/\[\s*([a-z]+)\s*\]/g, (_match: string, x: string) => `[${x}]`)
return response
}
response = response.replace(
/\[\s*([a-z]+)\s*\]/g,
(_match: string, x: string) => `[${x}]`
);
return response;
};
export const findCitations: (text: string, citations: Citation[]) => Map<string, Citation> = (
text,
citations
) => {
export const findCitations: (
text: string,
citations: Citation[]
) => Map<string, Citation> = (text, citations) => {
// figure out what citations are in the response, and map them appropriately
const cite_map = new Map<string, Citation>()
const cite_map = new Map<string, Citation>();
// scan a regex for [x] over the response. If x isn't in the map, add it.
// (note: we're actually doing this twice - once on parsing, once on render.
// if that looks like a problem, we could swap from strings to custom ropes).
const regex = /\[([a-z]+)\]/g
let match
const regex = /\[([a-z]+)\]/g;
let match;
while ((match = regex.exec(text)) !== null) {
const letter = match[1]
if (!letter || cite_map.has(letter!)) continue
const letter = match[1];
if (!letter || cite_map.has(letter!)) continue;
const citation = citations[letter.charCodeAt(0) - 'a'.charCodeAt(0)]
if (!citation) continue
const citation = citations[letter.charCodeAt(0) - "a".charCodeAt(0)];
if (!citation) continue;
cite_map.set(letter!, citation)
cite_map.set(letter!, citation);
}
return cite_map
}
return cite_map;
};
export const ShowCitation: React.FC<{citation: Citation}> = ({citation}) => {
var c_str = citation.title
export const ShowCitation: React.FC<{ citation: Citation }> = ({
citation,
}) => {
var c_str = citation.title;
if (citation.authors && citation.authors.length > 0) c_str += ' - ' + citation.authors.join(', ')
if (citation.date && citation.date !== '') c_str += ' - ' + citation.date
if (citation.authors && citation.authors.length > 0)
c_str += " - " + citation.authors.join(", ");
if (citation.date && citation.date !== "") c_str += " - " + citation.date;
// if we don't have a url, link to a duckduckgo search for the title instead
const url =
citation.url && citation.url !== ''
citation.url && citation.url !== ""
? citation.url
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
return (
<A
className={
Colours[(citation.index - 1) % Colours.length] +
' my-2 flex w-fit items-center rounded border-2 text-sm no-underline'
" my-2 flex w-fit items-center rounded border-2 text-sm no-underline"
}
href={url}
>
<span className="mx-1"> [{citation.index}] </span>
<p className="mx-1 my-0"> {c_str} </p>
</A>
)
}
);
};
export const CitationRef: React.FC<{citation?: Citation}> = ({citation}) => {
if (!citation) return null
export const CitationRef: React.FC<{ citation?: Citation }> = ({
citation,
}) => {
if (!citation) return null;
const url =
citation.url && citation.url !== ''
citation.url && citation.url !== ""
? citation.url
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
return (
<A
className={
Colours[(citation.index - 1) % Colours.length] +
' ml-1 mr-0.5 w-min rounded border-2 px-0.5 pb-0.5 text-sm no-underline'
" ml-1 mr-0.5 w-min rounded border-2 px-0.5 pb-0.5 text-sm no-underline"
}
href={url}
>
[{citation.index}]
</A>
)
}
);
};
export const CitationsBlock: React.FC<{
text: string
citations: Map<string, Citation>
textRenderer: (t: string) => any
}> = ({text, citations, textRenderer}) => {
const regex = /\[([a-z]+)\]/g
text: string;
citations: Map<string, Citation>;
textRenderer: (t: string) => any;
}> = ({ text, citations, textRenderer }) => {
const regex = /\[([a-z]+)\]/g;
return (
<p>
{' '}
{" "}
{text.split(regex).map((part, i) => {
// When splitting, the even parts are basic text sections, while the odd ones are
// citations
if (i % 2 == 0) {
return textRenderer(part)
return textRenderer(part);
} else {
return <CitationRef citation={citations.get(part)} key={i} />
return <CitationRef citation={citations.get(part)} key={i} />;
}
})}
</p>
)
}
);
};
+19 -10
View File
@@ -1,6 +1,12 @@
import type {Mode} from '../types'
import type { Mode } from "../types";
export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: any) => void}) => {
export const Controls = ({
mode,
setMode,
}: {
mode: [Mode, boolean];
setMode: (m: any) => void;
}) => {
{
/* three buttons for the three modes, place far right, 1rem between each */
}
@@ -8,10 +14,11 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
<div className="ml-auto mr-0 mb-5 flex w-fit flex-row justify-center gap-2">
<button
className={
'border border-gray-300 px-1 ' + (mode[1] && mode[0] === 'rookie' ? 'bg-gray-200' : '')
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "rookie" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(['rookie', true])
setMode(["rookie", true]);
}}
title="For people who are new to the field of AI alignment. The
answer might be longer, since technical terms will be
@@ -22,10 +29,11 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
</button>
<button
className={
'border border-gray-300 px-1 ' + (mode[1] && mode[0] === 'concise' ? 'bg-gray-200' : '')
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "concise" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(['concise', true])
setMode(["concise", true]);
}}
title="Quick and to the point. Followup questions may need to be
asked to get the full picture of what's going on."
@@ -34,15 +42,16 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
</button>
<button
className={
'border border-gray-300 px-1 ' + (mode[1] && mode[0] === 'default' ? 'bg-gray-200' : '')
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "default" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(['default', true])
setMode(["default", true]);
}}
title="A balanced default mode."
>
default
</button>
</div>
)
}
);
};
+40 -33
View File
@@ -4,14 +4,14 @@ import type {
ErrorMessage,
StampyMessage,
UserEntry,
} from '../types'
import {AssistantEntry} from './assistant'
import {GlossarySpan} from './glossary'
import Image from 'next/image'
import logo from '../logo.svg'
import TextareaAutosize from 'react-textarea-autosize'
} from "../types";
import { AssistantEntry } from "./assistant";
import { GlossarySpan } from "./glossary";
import Image from "next/image";
import logo from "../logo.svg";
import TextareaAutosize from "react-textarea-autosize";
export const User = ({entry}: {entry: UserEntry}) => {
export const User = ({ entry }: { entry: UserEntry }) => {
return (
<li className="mt-1 mb-2 flex">
<TextareaAutosize
@@ -19,59 +19,66 @@ export const User = ({entry}: {entry: UserEntry}) => {
value={entry.content}
/>
</li>
)
}
);
};
export const Error = ({entry}: {entry: ErrorMessage}) => {
export const Error = ({ entry }: { entry: ErrorMessage }) => {
return (
<li>
<p className="border border-red-500 bg-red-100 px-1 text-red-800"> {entry.content} </p>
<p className="border border-red-500 bg-red-100 px-1 text-red-800">
{" "}
{entry.content}{" "}
</p>
</li>
)
}
);
};
export const Assistant = ({entry}: {entry: AssistantEntryType}) => {
export const Assistant = ({ entry }: { entry: AssistantEntryType }) => {
return (
<li>
<AssistantEntry entry={entry} />
</li>
)
}
);
};
export const Stampy = ({entry}: {entry: StampyMessage}) => {
export const Stampy = ({ entry }: { entry: StampyMessage }) => {
return (
<li>
<div
className="my-7 rounded bg-slate-500 px-4 py-0.5 text-slate-50"
style={{
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '99.8%',
marginLeft: "auto",
marginRight: "auto",
maxWidth: "99.8%",
}}
>
<div>
<GlossarySpan content={entry.content} />
</div>
<div className="mb-3 flex justify-end">
<a href={entry.url} target="_blank" className="flex items-center space-x-1">
<a
href={entry.url}
target="_blank"
className="flex items-center space-x-1"
>
<span>aisafety.info</span>
<Image src={logo} alt="aisafety.info logo" width={19} />
</a>
</div>
</div>
</li>
)
}
);
};
export const Entry = ({entry}: {entry: EntryType}) => {
export const Entry = ({ entry }: { entry: EntryType }) => {
switch (entry.role) {
case 'user':
return <User entry={entry} />
case 'error':
return <Error entry={entry} />
case 'assistant':
return <Assistant entry={entry} />
case 'stampy':
return <Stampy entry={entry} />
case "user":
return <User entry={entry} />;
case "error":
return <Error entry={entry} />;
case "assistant":
return <Assistant entry={entry} />;
case "stampy":
return <Stampy entry={entry} />;
}
}
};
+23 -21
View File
@@ -1,30 +1,32 @@
import {createContext, useContext} from 'react'
import { createContext, useContext } from "react";
type GlossaryItem = {
term: string
pageid: string
contents: string
}
term: string;
pageid: string;
contents: string;
};
export type Glossary = Map<string, GlossaryItem>
export type Glossary = Map<string, GlossaryItem>;
export const GlossaryContext = createContext<{g: Glossary; r: RegExp} | null>(null)
export const GlossaryContext = createContext<{ g: Glossary; r: RegExp } | null>(
null
);
// A component which wraps arbitrary html in a span, and injects glossary terms
// into it as hoverable pop-up links. The text is immediately rendered normally,
// but after the glossary is loaded (which happens once per page, asynchronously),
// the glossary terms are replaced with elements.
export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
const g = useContext(GlossaryContext)
export const GlossarySpan: React.FC<{ content: string }> = ({ content }) => {
const g = useContext(GlossaryContext);
// If the glossary hasn't loaded yet, just render the text normally.
if (g == null) {
return <span dangerouslySetInnerHTML={{__html: content}} />
return <span dangerouslySetInnerHTML={{ __html: content }} />;
}
const glossary = g.g
const glossaryRegex = g.r
const glossary = g.g;
const glossaryRegex = g.r;
// Otherwise, replace glossary terms with links. We can do this in
// O(n * sum of term lengths) by finding String.prototype.indexOf of
@@ -36,17 +38,17 @@ export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
<span
dangerouslySetInnerHTML={{
__html: content.replace(glossaryRegex!, (match) => {
const item = glossary.get(match.toLowerCase())
if (item == undefined) return match
const item = glossary.get(match.toLowerCase());
if (item == undefined) return match;
const hover_content = item.contents
const pageid = item.pageid
const hover_content = item.contents;
const pageid = item.pageid;
if (pageid == undefined || pageid.trim() == '') {
if (pageid == undefined || pageid.trim() == "") {
return `
<div class="glossary-hover" nowrap>${hover_content}</div>
<span class="glossary-link">${match}</span>
`
`;
} else {
return `
<div class="glossary-hover" nowrap>${hover_content}</div>
@@ -55,10 +57,10 @@ export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
class="glossary-link">
${match}
</a>
`
`;
}
}),
}}
/>
)
}
);
};
+12 -10
View File
@@ -1,11 +1,13 @@
import React from 'react'
import Link from 'next/link'
import Image from 'next/image'
import logo from '../logo.svg'
import React from "react";
import Link from "next/link";
import Image from "next/image";
import logo from "../logo.svg";
const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) => {
const Header: React.FC<{ page: "index" | "semantic" | "playground" }> = ({
page,
}) => {
const sidebar =
page === 'index' ? (
page === "index" ? (
<span className="flex flex-1 flex-col justify-start text-right font-semibold">
<Link href="/semantic">Show Sources</Link>
</span>
@@ -13,7 +15,7 @@ const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) =
<span className="flex flex-1 flex-col justify-start text-right font-semibold">
<Link href="/">Go Chat</Link>
</span>
)
);
return (
<div className="my-4 flex">
@@ -21,7 +23,7 @@ const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) =
<h1 className="my-0 flex-1">AI Safety Chatbot</h1>
{sidebar}
</div>
)
}
);
};
export default Header
export default Header;
+17 -17
View File
@@ -5,28 +5,28 @@
// the source file for it to be included in the build
export const Colours = [
'bg-red-100 border-red-300 text-red-800',
'bg-amber-100 border-amber-300 text-amber-800',
'bg-orange-100 border-orange-300 text-orange-800',
'bg-lime-100 border-lime-300 text-lime-800',
'bg-green-100 border-green-300 text-green-800',
'bg-cyan-100 border-cyan-300 text-cyan-800',
'bg-blue-100 border-blue-300 text-blue-800',
'bg-violet-100 border-violet-300 text-violet-800',
'bg-pink-100 border-pink-300 text-pink-800',
]
"bg-red-100 border-red-300 text-red-800",
"bg-amber-100 border-amber-300 text-amber-800",
"bg-orange-100 border-orange-300 text-orange-800",
"bg-lime-100 border-lime-300 text-lime-800",
"bg-green-100 border-green-300 text-green-800",
"bg-cyan-100 border-cyan-300 text-cyan-800",
"bg-blue-100 border-blue-300 text-blue-800",
"bg-violet-100 border-violet-300 text-violet-800",
"bg-pink-100 border-pink-300 text-pink-800",
];
export const A: React.FC<{
href: string
className?: string
children: React.ReactNode
}> = ({href, className, children}) => {
href: string;
className?: string;
children: React.ReactNode;
}> = ({ href, className, children }) => {
// link element that only populates the href field if the contents are there
return href && href !== '' ? (
return href && href !== "" ? (
<a className={className} href={href} target="_blank" rel="noreferrer">
{children}
</a>
) : (
<a className={className}>{children}</a>
)
}
);
};
+10 -7
View File
@@ -1,8 +1,11 @@
import React, {ReactNode} from 'react'
import Head from 'next/head'
import Header from './header'
import React, { ReactNode } from "react";
import Head from "next/head";
import Header from "./header";
const Page: React.FC<{children: ReactNode; page: 'index' | 'semantic'}> = ({page, children}) => {
const Page: React.FC<{ children: ReactNode; page: "index" | "semantic" }> = ({
page,
children,
}) => {
return (
<>
<Head>
@@ -13,6 +16,6 @@ const Page: React.FC<{children: ReactNode; page: 'index' | 'semantic'}> = ({page
{children}
</main>
</>
)
}
export default Page
);
};
export default Page;
+67 -61
View File
@@ -1,100 +1,106 @@
import React from 'react'
import {useState, useEffect} from 'react'
import type {Followup} from '../types'
import TextareaAutosize from 'react-textarea-autosize'
import dynamic from 'next/dynamic'
import React from "react";
import { useState, useEffect } from "react";
import type { Followup } from "../types";
import TextareaAutosize from "react-textarea-autosize";
import dynamic from "next/dynamic";
// initial questions to fill the search box with.
export const initialQuestions: string[] = [
'Are there any regulatory efforts aimed at addressing AI safety and alignment concerns?',
'How can I help with AI safety and alignment?',
'How could a predictive model - like an LLM - act like an agent?',
"Are there any regulatory efforts aimed at addressing AI safety and alignment concerns?",
"How can I help with AI safety and alignment?",
"How could a predictive model - like an LLM - act like an agent?",
"How could an AI possibly be an x-risk when some populations aren't even connected to the internet?",
"I'm not convinced, why is this important?",
'Summarize the differences in opinion between Eliezer Yudkowsky and Paul Christiano.',
"Summarize the differences in opinion between Eliezer Yudkowsky and Paul Christiano.",
'What are "RAAPs"?',
'What are "scaling laws" and how are they relevant to safety?',
'What are some of the different research approaches?',
'What are the differences between Inner and Outer alignment?',
"What are some of the different research approaches?",
"What are the differences between Inner and Outer alignment?",
'What does the term "x-risk" mean?',
'What is "FOOM"?',
'What is "instrumental convergence"?',
'What is a hard takeoff?',
'What is a mesa-optimizer?',
'What is AI safety and alignment?',
'What is an AI arms race?',
'What is an Intelligence Explosion?',
"What is a hard takeoff?",
"What is a mesa-optimizer?",
"What is AI safety and alignment?",
"What is an AI arms race?",
"What is an Intelligence Explosion?",
'What is the "orthogonality thesis"?',
'Why would we expect AI to be "misaligned by default"?',
]
];
const SearchBoxInternal: React.FC<{
search: (
query: string,
query_source: 'search' | 'followups',
query_source: "search" | "followups",
enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void,
controller: AbortController
) => void
onQuery?: (q: string) => any
}> = ({search, onQuery}) => {
const initial_query = initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || ''
) => void;
onQuery?: (q: string) => any;
}> = ({ search, onQuery }) => {
const initial_query =
initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || "";
const [query, setQuery] = useState(initial_query)
const [loading, setLoading] = useState(false)
const [followups, setFollowups] = useState<Followup[]>([])
const [controller, setController] = useState(new AbortController())
const [query, setQuery] = useState(initial_query);
const [loading, setLoading] = useState(false);
const [followups, setFollowups] = useState<Followup[]>([]);
const [controller, setController] = useState(new AbortController());
const inputRef = React.useRef<HTMLTextAreaElement>(null)
const inputRef = React.useRef<HTMLTextAreaElement>(null);
// because everything is async, I can't just manually set state at the
// point we do a search. Instead it needs to be passed into the search
// method, for some reason.
const enable =
(controller: AbortController) => (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => {
if (!controller.signal.aborted) setQuery('')
(controller: AbortController) =>
(f_set: Followup[] | ((fs: Followup[]) => Followup[])) => {
if (!controller.signal.aborted) setQuery("");
setLoading(false)
setFollowups(f_set)
}
setLoading(false);
setFollowups(f_set);
};
useEffect(() => {
// set focus on the input box
if (!loading) inputRef.current?.focus()
}, [loading])
if (!loading) inputRef.current?.focus();
}, [loading]);
// on first mount focus and set cursor to end of input
useEffect(() => {
if (!inputRef.current) return
inputRef.current.focus()
inputRef.current.selectionStart = inputRef.current.textLength
inputRef.current.selectionEnd = inputRef.current.textLength
}, [])
if (!inputRef.current) return;
inputRef.current.focus();
inputRef.current.selectionStart = inputRef.current.textLength;
inputRef.current.selectionEnd = inputRef.current.textLength;
}, []);
const runSearch = (query: string, searchtype: 'search' | 'followups') => () => {
if (loading || query.trim() === '') return
const runSearch =
(query: string, searchtype: "search" | "followups") => () => {
if (loading || query.trim() === "") return;
setLoading(true)
const controller = new AbortController()
setController(controller)
search(query, 'search', enable(controller), controller)
}
const cancelSearch = () => controller.abort()
setLoading(true);
const controller = new AbortController();
setController(controller);
search(query, "search", enable(controller), controller);
};
const cancelSearch = () => controller.abort();
return (
<>
<div className="mt-1 flex flex-col items-end">
{' '}
{" "}
{followups.map((followup, i) => {
return (
<li key={i}>
<button
className="my-1 border border-gray-300 px-1"
onClick={runSearch(followup.pageid + '\n' + followup.text, 'followups')}
onClick={runSearch(
followup.pageid + "\n" + followup.text,
"followups"
)}
>
<span> {followup.text} </span>
</button>
</li>
)
);
})}
</div>
@@ -104,31 +110,31 @@ const SearchBoxInternal: React.FC<{
ref={inputRef}
value={query}
onChange={(e) => {
setQuery(e.target.value)
onQuery && onQuery(e.target.value)
setQuery(e.target.value);
onQuery && onQuery(e.target.value);
}}
onKeyDown={(e) => {
// if <esc>, blur the input box
if (e.key === 'Escape') e.currentTarget.blur()
if (e.key === "Escape") e.currentTarget.blur();
// if <enter> without <shift>, submit the form (if it's not empty)
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
runSearch(query, 'search')
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
runSearch(query, "search");
}
}}
/>
<button
className="ml-2"
type="button"
onClick={loading ? cancelSearch : runSearch(query, 'search')}
onClick={loading ? cancelSearch : runSearch(query, "search")}
>
{loading ? 'Cancel' : 'Search'}
{loading ? "Cancel" : "Search"}
</button>
</div>
</>
)
}
);
};
export const SearchBox = dynamic(() => Promise.resolve(SearchBoxInternal), {
ssr: false,
})
});
+107 -97
View File
@@ -1,4 +1,4 @@
import {API_URL, STAMPY_URL, STAMPY_CONTENT_URL} from '../settings'
import { API_URL, STAMPY_URL, STAMPY_CONTENT_URL } from "../settings";
import type {
Citation,
Entry,
@@ -9,48 +9,48 @@ import type {
CurrentSearch,
SearchResult,
LLMSettings,
} from '../types'
import {formatCitations, findCitations} from '../components/citations'
} from "../types";
import { formatCitations, findCitations } from "../components/citations";
const MAX_FOLLOWUPS = 4
const DATA_HEADER = 'data: '
const EVENT_END_HEADER = 'event: close'
const MAX_FOLLOWUPS = 4;
const DATA_HEADER = "data: ";
const EVENT_END_HEADER = "event: close";
type HistoryEntry = {
role: 'error' | 'stampy' | 'assistant' | 'user'
content: string
}
role: "error" | "stampy" | "assistant" | "user";
content: string;
};
const ignoreAbort = (error: Error) => {
if (error.name !== 'AbortError') {
throw error
if (error.name !== "AbortError") {
throw error;
}
}
};
export async function* iterateData(res: Response) {
const reader = res.body!.getReader()
var message = ''
const reader = res.body!.getReader();
var message = "";
while (true) {
const {done, value} = await reader.read()
const { done, value } = await reader.read();
if (done) return
if (done) return;
const chunk = new TextDecoder('utf-8').decode(value)
for (const line of chunk.split('\n')) {
const chunk = new TextDecoder("utf-8").decode(value);
for (const line of chunk.split("\n")) {
// Most times, it seems that a single read() call will be one SSE "message",
// but I'll do the proper aggregation spec thing in case that's not always true.
if (line.startsWith(EVENT_END_HEADER)) {
return
return;
} else if (line.startsWith(DATA_HEADER)) {
message += line.slice(DATA_HEADER.length)
message += line.slice(DATA_HEADER.length);
// Fixes #43
} else if (line !== '') {
message += line
} else if (message !== '') {
yield JSON.parse(message)
message = ''
} else if (line !== "") {
message += line;
} else if (message !== "") {
yield JSON.parse(message);
message = "";
}
}
}
@@ -61,50 +61,50 @@ export const extractAnswer = async (
setCurrent: (e: CurrentSearch) => void
): Promise<SearchResult> => {
var result: AssistantEntry = {
role: 'assistant',
content: '',
role: "assistant",
content: "",
citations: [],
citationsMap: new Map(),
}
var followups: Followup[] = []
};
var followups: Followup[] = [];
for await (var data of iterateData(res)) {
switch (data.state) {
case 'loading':
setCurrent({phase: data.phase, ...result})
break
case "loading":
setCurrent({ phase: data.phase, ...result });
break;
case 'citations':
case "citations":
result = {
...result,
citations: data?.citations || result?.citations || [],
}
setCurrent({phase: data.phase, ...result})
break
};
setCurrent({ phase: data.phase, ...result });
break;
case 'streaming':
case "streaming":
// incrementally build up the response
const content = formatCitations((result?.content || '') + data.content)
const content = formatCitations((result?.content || "") + data.content);
result = {
content,
role: 'assistant',
role: "assistant",
citations: result?.citations || [],
citationsMap: findCitations(content, result?.citations || []),
}
setCurrent({phase: 'streaming', ...result})
break
};
setCurrent({ phase: "streaming", ...result });
break;
case 'followups':
case "followups":
// add any potential followup questions
followups = data.followups.map((value: any) => value as Followup)
break
case 'done':
break
case 'error':
throw data.error
followups = data.followups.map((value: any) => value as Followup);
break;
case "done":
break;
case "error":
throw data.error;
}
}
return {result, followups}
}
return { result, followups };
};
const fetchLLM = async (
sessionId: string,
@@ -113,18 +113,18 @@ const fetchLLM = async (
history: HistoryEntry[],
controller: AbortController
): Promise<Response | void> =>
fetch(API_URL + '/chat', {
fetch(API_URL + "/chat", {
signal: controller.signal,
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
keepalive: true,
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({sessionId, query, history, settings}),
}).catch(ignoreAbort)
body: JSON.stringify({ sessionId, query, history, settings }),
}).catch(ignoreAbort);
export const queryLLM = async (
query: string,
@@ -135,58 +135,59 @@ export const queryLLM = async (
controller: AbortController
): Promise<SearchResult> => {
// do SSE on a POST request.
const res = await fetchLLM(sessionId, query, settings, history, controller)
const res = await fetchLLM(sessionId, query, settings, history, controller);
if (!res) {
return {result: {role: 'error', content: 'No response from server'}}
return { result: { role: "error", content: "No response from server" } };
} else if (!res.ok) {
return {result: {role: 'error', content: 'POST Error: ' + res.status}}
return { result: { role: "error", content: "POST Error: " + res.status } };
}
try {
return await extractAnswer(res, setCurrent)
return await extractAnswer(res, setCurrent);
} catch (e) {
if ((e as Error)?.name === 'AbortError') {
return {result: {role: 'error', content: 'aborted'}}
if ((e as Error)?.name === "AbortError") {
return { result: { role: "error", content: "aborted" } };
}
return {
result: {role: 'error', content: e ? e.toString() : 'unknown error'},
}
result: { role: "error", content: e ? e.toString() : "unknown error" },
};
}
}
};
const cleanStampyContent = (contents: string) =>
contents.replace(
/<a(.*?)href="\/\?state=([a-zA-Z0-9]+.*?)"(.*?)<\/a>/g,
(_, pre, linkParts, post) => `<a${pre}href="${STAMPY_URL}/?state=${linkParts}"${post}</a>`
)
(_, pre, linkParts, post) =>
`<a${pre}href="${STAMPY_URL}/?state=${linkParts}"${post}</a>`
);
export const getStampyContent = async (
questionId: string,
controller: AbortController
): Promise<SearchResult> => {
const res = await fetch(`${STAMPY_CONTENT_URL}/${questionId}`, {
method: 'GET',
method: "GET",
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
"Content-Type": "application/json",
Accept: "application/json",
},
}).catch(ignoreAbort)
}).catch(ignoreAbort);
if (!res) {
return {result: {role: 'error', content: 'No response from server'}}
return { result: { role: "error", content: "No response from server" } };
} else if (!res.ok) {
return {result: {role: 'error', content: 'POST Error: ' + res.status}}
return { result: { role: "error", content: "POST Error: " + res.status } };
}
const data = (await res.json()).data
const data = (await res.json()).data;
let result = {
role: 'stampy',
role: "stampy",
content: cleanStampyContent(data.text),
url: `${STAMPY_URL}/?state=${data.pageid}`,
} as StampyMessage
} as StampyMessage;
// re-enable the searchbox, with the question that was just answered
// removed from the list of possible followups.
@@ -196,45 +197,54 @@ export const getStampyContent = async (
pageid: f.pageid!,
text: f.title!,
score: 0,
}))
}));
const fpids = new Set(f_new.map((f: Followup) => f.pageid))
const fpids = new Set(f_new.map((f: Followup) => f.pageid));
const followups = (f_old: Followup[]): Followup[] => {
const f_old_filtered = f_old.filter((f) => f.pageid !== data.pageid && !fpids.has(f.pageid))
return [...f_new, ...f_old_filtered].slice(0, MAX_FOLLOWUPS)
}
const f_old_filtered = f_old.filter(
(f) => f.pageid !== data.pageid && !fpids.has(f.pageid)
);
return [...f_new, ...f_old_filtered].slice(0, MAX_FOLLOWUPS);
};
return {followups, result}
}
return { followups, result };
};
export const runSearch = async (
query: string,
query_source: 'search' | 'followups',
query_source: "search" | "followups",
settings: LLMSettings,
entries: Entry[],
setCurrent: (c: CurrentSearch) => void,
sessionId: string,
controller: AbortController
): Promise<SearchResult> => {
if (query_source === 'search') {
if (query_source === "search") {
const history = entries
.filter((entry) => entry.role !== 'error')
.filter((entry) => entry.role !== "error")
.map((entry) => ({
role: entry.role,
content: entry.content.trim(),
}))
}));
return await queryLLM(query, settings, history, setCurrent, sessionId, controller)
return await queryLLM(
query,
settings,
history,
setCurrent,
sessionId,
controller
);
} else {
// ----------------- HUMAN AUTHORED CONTENT RETRIEVAL ------------------
const [questionId] = query.split('\n', 2)
const [questionId] = query.split("\n", 2);
if (questionId) {
return await getStampyContent(questionId, controller)
return await getStampyContent(questionId, controller);
}
const result = {
role: 'error',
content: 'Could not extract Stampy id from ' + query,
}
return {result} as SearchResult
role: "error",
content: "Could not extract Stampy id from " + query,
};
return { result } as SearchResult;
}
}
};
+75 -73
View File
@@ -1,127 +1,129 @@
import {type AppType} from 'next/dist/shared/lib/utils'
import {useEffect, useState} from 'react'
import { type AppType } from "next/dist/shared/lib/utils";
import { useEffect, useState } from "react";
import '~/styles/globals.css'
import "~/styles/globals.css";
import {Glossary, GlossaryContext} from '../components/glossary'
import { Glossary, GlossaryContext } from "../components/glossary";
const MyApp: AppType = ({Component, pageProps}) => {
const [glossary, setGlossary] = useState<{g: Glossary; r: RegExp} | null>(null)
const MyApp: AppType = ({ Component, pageProps }) => {
const [glossary, setGlossary] = useState<{ g: Glossary; r: RegExp } | null>(
null
);
// fetch glossary and compile regex once on load
useEffect(() => {
if (glossary === null)
tempHackFetch('/questions/glossary')
tempHackFetch("/questions/glossary")
.then((res) => res.json())
.then((data) => {
const glossary: Glossary = new Map(Object.entries(data))
const glossary: Glossary = new Map(Object.entries(data));
const keys = Array.from(glossary.keys())
.sort((a, b) => b.length - a.length) // sort by length descending
.map((k) => k.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')) // escape regex chars
.map((k) => `\\b${k}\\b`) // add word boundaries
.map((k) => k.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")) // escape regex chars
.map((k) => `\\b${k}\\b`); // add word boundaries
const regex = new RegExp(keys.join('|'), 'gim')
setGlossary({g: glossary, r: regex})
})
}, [])
const regex = new RegExp(keys.join("|"), "gim");
setGlossary({ g: glossary, r: regex });
});
}, []);
return (
<GlossaryContext.Provider value={glossary}>
<Component {...pageProps} />
</GlossaryContext.Provider>
)
}
);
};
export default MyApp
export default MyApp;
// ------------------- hack until server endpoint is working -------------------
const GLOSSARY_JSON = {
'chain of thought prompting': {
term: 'chain of thought prompting',
pageid: '8EL7',
"chain of thought prompting": {
term: "chain of thought prompting",
pageid: "8EL7",
contents:
'<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n',
"<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n",
},
'chain-of-thought': {
term: 'chain-of-thought',
pageid: '8EL7',
"chain-of-thought": {
term: "chain-of-thought",
pageid: "8EL7",
contents:
'<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n',
"<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n",
},
"goodhart's law": {
term: "goodhart's law",
pageid: '8185',
pageid: "8185",
contents:
'<p>Goodharts law states that when a measure becomes a target, it ceases to be a good measure.</p>\n',
"<p>Goodharts law states that when a measure becomes a target, it ceases to be a good measure.</p>\n",
},
'the big g,': {
term: 'the big g,',
pageid: '8185',
"the big g,": {
term: "the big g,",
pageid: "8185",
contents:
'<p>Goodharts law states that when a measure becomes a target, it ceases to be a good measure.</p>\n',
"<p>Goodharts law states that when a measure becomes a target, it ceases to be a good measure.</p>\n",
},
'terminal goals': {
term: 'terminal goals',
pageid: '',
"terminal goals": {
term: "terminal goals",
pageid: "",
contents:
'<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n',
"<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n",
},
'terminal goal': {
term: 'terminal goal',
pageid: '',
"terminal goal": {
term: "terminal goal",
pageid: "",
contents:
'<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n',
"<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n",
},
'orthogonality thesis': {
term: 'orthogonality thesis',
pageid: '6568',
"orthogonality thesis": {
term: "orthogonality thesis",
pageid: "6568",
contents:
'<p>The thesis that any level of intelligence is compatible with any terminal goals. This implies that intelligence alone is not enough to make a system moral.</p>\n',
"<p>The thesis that any level of intelligence is compatible with any terminal goals. This implies that intelligence alone is not enough to make a system moral.</p>\n",
},
'instrumental convergence': {
term: 'instrumental convergence',
pageid: '897I',
"instrumental convergence": {
term: "instrumental convergence",
pageid: "897I",
contents:
'<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n',
"<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n",
},
'instrumentally convergent goals': {
term: 'instrumentally convergent goals',
pageid: '897I',
"instrumentally convergent goals": {
term: "instrumentally convergent goals",
pageid: "897I",
contents:
'<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n',
"<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n",
},
llm: {
term: 'llm',
pageid: '',
term: "llm",
pageid: "",
contents:
'<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n',
"<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n",
},
'large language model': {
term: 'large language model',
pageid: '',
"large language model": {
term: "large language model",
pageid: "",
contents:
'<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n',
"<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n",
},
'goal misgeneralization': {
term: 'goal misgeneralization',
pageid: '',
"goal misgeneralization": {
term: "goal misgeneralization",
pageid: "",
contents:
'<p>pursuing a different goal during deployment from the one that was pursued during training due to distribution shift</p>\n',
"<p>pursuing a different goal during deployment from the one that was pursued during training due to distribution shift</p>\n",
},
interpretability: {
term: 'interpretability',
pageid: '8241',
term: "interpretability",
pageid: "8241",
contents:
'<p>Interpretability is an area of alignment research that aims to make machine learning systems easier for humans to understand.</p>\n',
"<p>Interpretability is an area of alignment research that aims to make machine learning systems easier for humans to understand.</p>\n",
},
'existential risk': {
term: 'existential risk',
pageid: '89LL',
"existential risk": {
term: "existential risk",
pageid: "89LL",
contents:
"<p>risks that threaten the destruction of humanity's long-term potential, including human extinction</p>\n",
},
}
};
const tempHackFetch = (_url: string) => {
return new Promise<Response>((resolve, _reject) => {
@@ -129,7 +131,7 @@ const tempHackFetch = (_url: string) => {
resolve({
ok: true,
json: () => Promise.resolve(GLOSSARY_JSON),
} as unknown as Response)
}, 1000)
})
}
} as unknown as Response);
}, 1000);
});
};
+23 -23
View File
@@ -1,46 +1,46 @@
import {type NextPage} from 'next'
import {useState, useEffect} from 'react'
import Link from 'next/link'
import { type NextPage } from "next";
import { useState, useEffect } from "react";
import Link from "next/link";
import {queryLLM, getStampyContent, runSearch} from '../hooks/useSearch'
import type {Mode} from '../types'
import Page from '../components/page'
import Chat from '../components/chat'
import {Controls} from '../components/controls'
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
import type { Mode } from "../types";
import Page from "../components/page";
import Chat from "../components/chat";
import { Controls } from "../components/controls";
const MAX_FOLLOWUPS = 4
const MAX_FOLLOWUPS = 4;
const Home: NextPage = () => {
const [sessionId, setSessionId] = useState('')
const [mode, setMode] = useState<[Mode, boolean]>(['default', false])
const [sessionId, setSessionId] = useState("");
const [mode, setMode] = useState<[Mode, boolean]>(["default", false]);
// store mode in localstorage
useEffect(() => {
if (mode[1]) localStorage.setItem('chat_mode', mode[0])
}, [mode])
if (mode[1]) localStorage.setItem("chat_mode", mode[0]);
}, [mode]);
// initial load
useEffect(() => {
const mode = (localStorage.getItem('chat_mode') as Mode) || 'default'
setMode([mode, true])
setSessionId(crypto.randomUUID())
}, [])
const mode = (localStorage.getItem("chat_mode") as Mode) || "default";
setMode([mode, true]);
setSessionId(crypto.randomUUID());
}, []);
return (
<Page page="index">
<Controls mode={mode} setMode={setMode} />
<h2 className="bg-red-100 text-red-800">
<b>WARNING</b>: This is a very <b>early prototype</b>.{' '}
<b>WARNING</b>: This is a very <b>early prototype</b>.{" "}
<Link href="http://bit.ly/stampy-chat-issues" target="_blank">
Feedback
</Link>{' '}
</Link>{" "}
welcomed.
</h2>
<Chat sessionId={sessionId} settings={{mode: mode[0]}} />
<Chat sessionId={sessionId} settings={{ mode: mode[0] }} />
</Page>
)
}
);
};
export default Home
export default Home;
+151 -126
View File
@@ -1,108 +1,117 @@
import type {NextPage} from 'next'
import {useState, useEffect, ChangeEvent} from 'react'
import TextareaAutosize from 'react-textarea-autosize'
import Head from 'next/head'
import Link from 'next/link'
import type { NextPage } from "next";
import { useState, useEffect, ChangeEvent } from "react";
import TextareaAutosize from "react-textarea-autosize";
import Head from "next/head";
import Link from "next/link";
import {queryLLM, getStampyContent, runSearch} from '../hooks/useSearch'
import type {Mode, Entry, LLMSettings} from '../types'
import Header from '../components/header'
import Chat from '../components/chat'
import {Controls} from '../components/controls'
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
import type { Mode, Entry, LLMSettings } from "../types";
import Header from "../components/header";
import Chat from "../components/chat";
import { Controls } from "../components/controls";
const MAX_FOLLOWUPS = 4
const MAX_FOLLOWUPS = 4;
const DEFAULT_PROMPTS = {
source: {
prefix:
'You are a helpful assistant knowledgeable about AI Alignment and Safety. ' +
"You are a helpful assistant knowledgeable about AI Alignment and Safety. " +
'Please give a clear and coherent answer to the user\'s questions.(written after "Q:") ' +
'using the following sources. Each source is labeled with a letter. Feel free to ' +
'use the sources in any order, and try to use multiple sources in your answers.\n\n',
"using the following sources. Each source is labeled with a letter. Feel free to " +
"use the sources in any order, and try to use multiple sources in your answers.\n\n",
suffix:
'\n\n' +
"\n\n" +
'Before the question ("Q: "), there will be a history of previous questions and answers. ' +
'These sources only apply to the last question. Any sources used in previous answers ' +
'are invalid.',
"These sources only apply to the last question. Any sources used in previous answers " +
"are invalid.",
},
question:
'In your answer, please cite any claims you make back to each source ' +
'using the format: [a], [b], etc. If you use multiple sources to make a claim ' +
"In your answer, please cite any claims you make back to each source " +
"using the format: [a], [b], etc. If you use multiple sources to make a claim " +
'cite all of them. For example: "AGI is concerning [c, d, e]."\n\n',
modes: {
default: '',
default: "",
concise:
'Answer very concisely, getting to the crux of the matter in as ' +
'few words as possible. Limit your answer to 1-2 sentences.\n\n',
"Answer very concisely, getting to the crux of the matter in as " +
"few words as possible. Limit your answer to 1-2 sentences.\n\n",
rookie:
"This user is new to the field of AI Alignment and Safety - don't " +
'assume they know any technical terms or jargon. Still give a complete answer ' +
'without patronizing the user, but take any extra time needed to ' +
'explain new concepts or to illustrate your answer with examples. ' +
'Put extra effort into explaining the intuition behind concepts ' +
'rather than just giving a formal definition.\n\n',
"assume they know any technical terms or jargon. Still give a complete answer " +
"without patronizing the user, but take any extra time needed to " +
"explain new concepts or to illustrate your answer with examples. " +
"Put extra effort into explaining the intuition behind concepts " +
"rather than just giving a formal definition.\n\n",
},
}
};
const MODELS = {
'gpt-3.5-turbo': {numTokens: 4095, topKBlocks: 10},
'gpt-3.5-turbo-16k': {numTokens: 16385, topKBlocks: 30},
'gpt-4': {numTokens: 8192, topKBlocks: 20},
"gpt-3.5-turbo": { numTokens: 4095, topKBlocks: 10 },
"gpt-3.5-turbo-16k": { numTokens: 16385, topKBlocks: 30 },
"gpt-4": { numTokens: 8192, topKBlocks: 20 },
/* 'gpt-4-32k': {numTokens: 32768, topKBlocks: 30}, */
}
};
const DEFAULT_SETTINGS = {
prompts: DEFAULT_PROMPTS,
mode: 'default' as Mode,
completions: 'gpt-3.5-turbo',
encoder: 'cl100k_base',
topKBlocks: MODELS['gpt-3.5-turbo'].topKBlocks, // the number of blocks to use as citations
numTokens: MODELS['gpt-3.5-turbo'].numTokens,
mode: "default" as Mode,
completions: "gpt-3.5-turbo",
encoder: "cl100k_base",
topKBlocks: MODELS["gpt-3.5-turbo"].topKBlocks, // the number of blocks to use as citations
numTokens: MODELS["gpt-3.5-turbo"].numTokens,
tokensBuffer: 50, // the number of tokens to leave as a buffer when calculating remaining tokens
maxHistory: 10, // the max number of previous items to use as history
historyFraction: 0.25, // the (approximate) fraction of num_tokens to use for history text before truncating
contextFraction: 0.5, // the (approximate) fraction of num_tokens to use for context text before truncating
}
const ENCODERS = ['cl100k_base']
};
const ENCODERS = ["cl100k_base"];
const updateIn = (obj: {[key: string]: any}, [head, ...rest]: string[], val: any) => {
const updateIn = (
obj: { [key: string]: any },
[head, ...rest]: string[],
val: any
) => {
if (!head) {
// No path provided - do nothing
} else if (!rest || rest.length == 0) {
obj[head] = val
obj[head] = val;
} else {
updateIn(obj[head], rest, val)
updateIn(obj[head], rest, val);
}
return obj
}
return obj;
};
type Parseable = string | number | undefined
type NumberParser = (v: Parseable) => number
type Parseable = string | number | undefined;
type NumberParser = (v: Parseable) => number;
type InputFields = {
field: string
label: string
value?: Parseable
min?: string | number
max?: string | number
step?: string | number
parser?: NumberParser
updater: (v: any) => any
}
field: string;
label: string;
value?: Parseable;
min?: string | number;
max?: string | number;
step?: string | number;
parser?: NumberParser;
updater: (v: any) => any;
};
const between =
(min: Parseable, max: Parseable, parser: NumberParser, updater: (v: any) => any) =>
(
min: Parseable,
max: Parseable,
parser: NumberParser,
updater: (v: any) => any
) =>
(event: ChangeEvent) => {
let num = parser((event.target as HTMLInputElement).value)
let num = parser((event.target as HTMLInputElement).value);
if (isNaN(num)) {
return
return;
} else if (min !== undefined && num < parser(min)) {
num = parser(min)
num = parser(min);
} else if (max !== undefined && num > parser(max)) {
num = parser(max)
num = parser(max);
}
updater(num)
}
updater(num);
};
const SectionHeader = ({text}: {text: string}) => (
const SectionHeader = ({ text }: { text: string }) => (
<h4 className="col-span-4 text-lg font-semibold">{text}</h4>
)
);
const NumberInput = ({
field,
@@ -116,7 +125,7 @@ const NumberInput = ({
}: InputFields) => (
<>
<label htmlFor={field} className="col-span-3 inline-block">
{label}:{' '}
{label}:{" "}
</label>
<input
name={field}
@@ -126,7 +135,7 @@ const NumberInput = ({
type="number"
/>
</>
)
);
const Slider = ({
field,
@@ -154,25 +163,26 @@ const Slider = ({
step={step}
/>
</>
)
);
type ChatSettingsParams = {
settings: LLMSettings
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void
}
settings: LLMSettings;
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void;
};
const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
const ChatSettings = ({ settings, updateSettings }: ChatSettingsParams) => {
const changeVal = (field: string, value: any) =>
updateSettings((prev) => ({...prev, [field]: value}))
updateSettings((prev) => ({ ...prev, [field]: value }));
const update = (setting: string) => (event: ChangeEvent) => {
changeVal(setting, (event.target as HTMLInputElement).value)
}
const updateNum = (field: string) => (num: Parseable) => changeVal(field, num)
changeVal(setting, (event.target as HTMLInputElement).value);
};
const updateNum = (field: string) => (num: Parseable) =>
changeVal(field, num);
return (
<div
className="chat-settings mx-5 grid w-[400px] flex-none grid-cols-4 gap-4 border-2 outline-black"
style={{height: 'fit-content'}}
style={{ height: "fit-content" }}
>
<SectionHeader text="Models" />
<label htmlFor="completions-model" className="col-span-2">
@@ -183,20 +193,26 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
className="col-span-2"
value={settings.completions}
onChange={(event: ChangeEvent) => {
const value = (event.target as HTMLInputElement).value
const {numTokens, topKBlocks} = MODELS[value as keyof typeof MODELS]
const prevNumTokens = MODELS[settings.completions as keyof typeof MODELS].numTokens
const prevTopKBlocks = MODELS[settings.completions as keyof typeof MODELS].topKBlocks
const value = (event.target as HTMLInputElement).value;
const { numTokens, topKBlocks } =
MODELS[value as keyof typeof MODELS];
const prevNumTokens =
MODELS[settings.completions as keyof typeof MODELS].numTokens;
const prevTopKBlocks =
MODELS[settings.completions as keyof typeof MODELS].topKBlocks;
if (settings.numTokens === prevNumTokens) {
changeVal('numTokens', numTokens)
changeVal("numTokens", numTokens);
} else {
changeVal('numTokens', Math.min(settings.numTokens || 0, numTokens))
changeVal(
"numTokens",
Math.min(settings.numTokens || 0, numTokens)
);
}
if (settings.topKBlocks === prevTopKBlocks) {
changeVal('topKBlocks', topKBlocks)
changeVal("topKBlocks", topKBlocks);
}
changeVal('completions', value)
changeVal("completions", value);
}}
>
{Object.keys(MODELS).map((name) => (
@@ -213,7 +229,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
name="encoder"
className="col-span-2"
value={settings.encoder}
onChange={update('encoder')}
onChange={update("encoder")}
>
{ENCODERS.map((name) => (
<option value={name} key={name}>
@@ -229,7 +245,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
label="Tokens"
min="1"
max={MODELS[settings.completions as keyof typeof MODELS].numTokens}
updater={updateNum('numTokens')}
updater={updateNum("numTokens")}
/>
<NumberInput
field="tokensBuffer"
@@ -237,7 +253,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
label="Number of tokens to leave as a buffer when calculating remaining tokens"
min="0"
max={settings.numTokens}
updater={updateNum('tokensBuffer')}
updater={updateNum("tokensBuffer")}
/>
<SectionHeader text="Prompt options" />
@@ -246,48 +262,57 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
field="topKBlocks"
label="Number of blocks to use as citations"
min="1"
updater={updateNum('topKBlocks')}
updater={updateNum("topKBlocks")}
/>
<NumberInput
value={settings.maxHistory}
field="maxHistory"
label="The max number of previous interactions to use"
min="0"
updater={updateNum('maxHistory')}
updater={updateNum("maxHistory")}
/>
<Slider
value={settings.contextFraction}
field="contextFraction"
label="Approximate fraction of num_tokens to use for citations text before truncating"
updater={updateNum('contextFraction')}
updater={updateNum("contextFraction")}
/>
<Slider
value={settings.historyFraction}
field="historyFraction"
label="Approximate fraction of num_tokens to use for history text before truncating"
updater={updateNum('historyFraction')}
updater={updateNum("historyFraction")}
/>
</div>
)
}
);
};
type ChatPromptParams = {
settings: LLMSettings
query: string
history: Entry[]
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void
}
settings: LLMSettings;
query: string;
history: Entry[];
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void;
};
const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParams) => {
const ChatPrompts = ({
settings,
query,
history,
updateSettings,
}: ChatPromptParams) => {
const updatePrompt =
(...path: string[]) =>
(event: ChangeEvent) => {
const newPrompts = {
...updateIn(settings.prompts || {}, path, (event.target as HTMLInputElement).value),
}
updateSettings((settings) => ({...settings, prompts: newPrompts}))
}
...updateIn(
settings.prompts || {},
path,
(event.target as HTMLInputElement).value
),
};
updateSettings((settings) => ({ ...settings, prompts: newPrompts }));
};
return (
<div className="chat-prompts mx-5 w-[400px] flex-none border-2 p-5 outline-black">
@@ -296,14 +321,14 @@ const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParam
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.source?.prefix}
onChange={updatePrompt('source', 'prefix')}
onChange={updatePrompt("source", "prefix")}
/>
<div>(This is where sources will be injected)</div>
{history.length > 0 && (
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.source?.suffix}
onChange={updatePrompt('source', 'suffix')}
onChange={updatePrompt("source", "suffix")}
/>
)}
</details>
@@ -324,48 +349,48 @@ const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParam
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.question}
onChange={updatePrompt('question')}
onChange={updatePrompt("question")}
/>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.modes[settings.mode || 'default']}
onChange={updatePrompt('modes', settings.mode || 'default')}
value={settings?.prompts?.modes[settings.mode || "default"]}
onChange={updatePrompt("modes", settings.mode || "default")}
/>
</details>
<div>Q: {query}</div>
</div>
)
}
);
};
const Playground: NextPage = () => {
const [sessionId, setSessionId] = useState('')
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS)
const [sessionId, setSessionId] = useState("");
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS);
const [query, setQuery] = useState<string>('')
const [history, setHistory] = useState<Entry[]>([])
const [query, setQuery] = useState<string>("");
const [history, setHistory] = useState<Entry[]>([]);
const setMode = (mode: [Mode, boolean]) => {
if (mode[1]) {
localStorage.setItem('chat_mode', mode[0])
updateSettings((settings) => ({...settings, mode: mode[0]}))
localStorage.setItem("chat_mode", mode[0]);
updateSettings((settings) => ({ ...settings, mode: mode[0] }));
}
}
};
// initial load
useEffect(() => {
const mode = (localStorage.getItem('chat_mode') as Mode) || 'default'
setMode([mode, true])
setSessionId(crypto.randomUUID())
}, [])
const mode = (localStorage.getItem("chat_mode") as Mode) || "default";
setMode([mode, true]);
setSessionId(crypto.randomUUID());
}, []);
return (
<>
<Head>
<title>AI Safety Info</title>
</Head>
<main style={{maxWidth: 'none'}}>
<main style={{ maxWidth: "none" }}>
<Header page="playground" />
<Controls mode={[settings.mode || 'default', true]} setMode={setMode} />
<Controls mode={[settings.mode || "default", true]} setMode={setMode} />
<div className="flex">
<ChatPrompts
settings={settings}
@@ -383,7 +408,7 @@ const Playground: NextPage = () => {
</div>
</main>
</>
)
}
);
};
export default Playground
export default Playground;
+47 -47
View File
@@ -1,47 +1,47 @@
import {type NextPage} from 'next'
import React, {useState} from 'react'
import {API_URL} from '../settings'
import type {Followup} from '../types'
import Page from '../components/page'
import {SearchBox} from '../components/searchbox'
import { type NextPage } from "next";
import React, { useState } from "react";
import { API_URL } from "../settings";
import type { Followup } from "../types";
import Page from "../components/page";
import { SearchBox } from "../components/searchbox";
const ignoreAbort = (error: Error) => {
if (error.name !== 'AbortError') {
throw error
if (error.name !== "AbortError") {
throw error;
}
}
};
const Semantic: NextPage = () => {
const [results, setResults] = useState<SemanticEntry[]>([])
const [results, setResults] = useState<SemanticEntry[]>([]);
const semantic_search = async (
query: string,
_query_source: 'search' | 'followups',
_query_source: "search" | "followups",
enable: (f_set: Followup[]) => void,
controller: AbortController
) => {
const res = await fetch(API_URL + '/semantic', {
method: 'POST',
const res = await fetch(API_URL + "/semantic", {
method: "POST",
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({query: query}),
}).catch(ignoreAbort)
body: JSON.stringify({ query: query }),
}).catch(ignoreAbort);
if (!res) {
enable([])
return
enable([]);
return;
} else if (!res.ok) {
console.error('load failure: ' + res.status)
console.error("load failure: " + res.status);
}
enable([])
enable([]);
const data = await res.json()
const data = await res.json();
setResults(data)
}
setResults(data);
};
return (
<Page page="semantic">
@@ -49,14 +49,14 @@ const Semantic: NextPage = () => {
<SearchBox search={semantic_search} />
<ul>
{results.map((entry, i) => (
<li key={'entry' + i}>
<li key={"entry" + i}>
<ShowSemanticEntry entry={entry} />
</li>
))}
</ul>
</Page>
)
}
);
};
// Round trip test. If this works, our heavier usecase probably will (famous last words)
// The one real difference is we'll want to send back a series of results as we get
@@ -64,39 +64,39 @@ const Semantic: NextPage = () => {
// shouldn't be too much harder.
type SemanticEntry = {
title: string
authors: string[]
date: string
url: string
tags: string
text: string
}
title: string;
authors: string[];
date: string;
url: string;
tags: string;
text: string;
};
const ShowSemanticEntry: React.FC<{entry: SemanticEntry}> = ({entry}) => {
const ShowSemanticEntry: React.FC<{ entry: SemanticEntry }> = ({ entry }) => {
return (
<div className="my-3">
{/* horizontally split first row, title on left, authors on right */}
<div className="flex">
<h3 className="flex-1 text-xl">{entry.title}</h3>
<p className="my-0 flex-1 text-right">
{entry.authors.join(', ')} - {entry.date}
{entry.authors.join(", ")} - {entry.date}
</p>
</div>
{entry.text.split('\n').map((paragraph, i) => {
const p = paragraph.trim()
if (p === '') return <></>
if (p === '.....') return <hr key={'b' + i} />
{entry.text.split("\n").map((paragraph, i) => {
const p = paragraph.trim();
if (p === "") return <></>;
if (p === ".....") return <hr key={"b" + i} />;
return (
<p className="text-sm" key={'p' + i}>
{' '}
{paragraph}{' '}
<p className="text-sm" key={"p" + i}>
{" "}
{paragraph}{" "}
</p>
)
);
})}
<a href={entry.url}>Read more</a>
</div>
)
}
);
};
export default Semantic
export default Semantic;
+5 -3
View File
@@ -1,3 +1,5 @@
export const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'
export const STAMPY_URL = process.env.STAMPY_URL || 'https://aisafety.info'
export const STAMPY_CONTENT_URL = process.env.STAMPY_CONTENT_URL || `${API_URL}/human`
export const API_URL =
process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:3001";
export const STAMPY_URL = process.env.STAMPY_URL || "https://aisafety.info";
export const STAMPY_CONTENT_URL =
process.env.STAMPY_CONTENT_URL || `${API_URL}/human`;
+44 -44
View File
@@ -1,62 +1,62 @@
export type Citation = {
title: string
authors: string[]
date: string
url: string
index: number
}
title: string;
authors: string[];
date: string;
url: string;
index: number;
};
export type Followup = {
text: string
pageid: string
score: number
}
text: string;
pageid: string;
score: number;
};
export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage
export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage;
export type UserEntry = {
role: 'user'
content: string
}
role: "user";
content: string;
};
export type AssistantEntry = {
role: 'assistant'
content: string
citations: Citation[]
citationsMap: Map<string, Citation>
}
role: "assistant";
content: string;
citations: Citation[];
citationsMap: Map<string, Citation>;
};
export type ErrorMessage = {
role: 'error'
content: string
}
role: "error";
content: string;
};
export type StampyMessage = {
role: 'stampy'
content: string
url: string
}
role: "stampy";
content: string;
url: string;
};
export type SearchResult = {
followups?: Followup[] | ((f: Followup[]) => Followup[])
result: Entry
}
export type CurrentSearch = (AssistantEntry & {phase?: string}) | undefined
followups?: Followup[] | ((f: Followup[]) => Followup[]);
result: Entry;
};
export type CurrentSearch = (AssistantEntry & { phase?: string }) | undefined;
export type Mode = 'rookie' | 'concise' | 'default'
export type Mode = "rookie" | "concise" | "default";
export type LLMSettings = {
prompts?: {
[key: string]: any
}
mode?: Mode
completions?: string
encoder?: string
topKBlocks?: number
numTokens?: number
tokensBuffer?: number
maxHistory?: number
historyFraction?: number
contextFraction?: number
[key: string]: any
}
[key: string]: any;
};
mode?: Mode;
completions?: string;
encoder?: string;
topKBlocks?: number;
numTokens?: number;
tokensBuffer?: number;
maxHistory?: number;
historyFraction?: number;
contextFraction?: number;
[key: string]: any;
};