diff --git a/web/src/components/chat.tsx b/web/src/components/chat.tsx index 7701524..2e335da 100644 --- a/web/src/components/chat.tsx +++ b/web/src/components/chat.tsx @@ -1,11 +1,12 @@ import { useState, useEffect } from "react"; import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; + import type { CurrentSearch, Citation, Entry, AssistantEntry as AssistantEntryType, - Mode, + LLMSettings, Followup, } from "../types"; import { SearchBox } from "../components/searchbox"; @@ -39,7 +40,14 @@ function scroll30() { window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); } -const Chat = ({ sessionId, mode }: { sessionId: string; mode: Mode }) => { +type ChatParams = { + sessionId: string; + settings: LLMSettings; + onQuery?: (q: string) => any; + onNewEntry?: (history: Entry[]) => any; +}; + +const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => { const [entries, setEntries] = useState([]); const [current, setCurrent] = useState(); const [citations, setCitations] = useState([]); @@ -82,6 +90,16 @@ const Chat = ({ sessionId, mode }: { sessionId: string; mode: Mode }) => { setCurrent(current); }; + const addEntry = (entry: Entry) => { + setEntries((prev) => { + const entries = [...prev, entry]; + if (onNewEntry) { + onNewEntry(entries); + } + return entries; + }); + }; + const search = async ( query: string, query_source: "search" | "followups", @@ -93,20 +111,20 @@ const Chat = ({ sessionId, mode }: { sessionId: string; mode: Mode }) => { role: "user", content: query_source === "search" ? query : query.split("\n", 2)[1]!, }; - setEntries((prev) => [...prev, userEntry]); + addEntry(userEntry); disable(); const { result, followups } = await runSearch( query, query_source, - mode, + settings.mode, entries, updateCurrent, sessionId ); setCurrent(undefined); - setEntries((prev) => [...prev, result]); + addEntry(result); enable(followups || []); scroll30(); }; @@ -137,11 +155,11 @@ const Chat = ({ sessionId, mode }: { sessionId: string; mode: Mode }) => { } return ( -
    +
      {entries.map((entry, i) => ( ))} - + {last_entry}
    diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 9f10fc7..1941361 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -3,7 +3,9 @@ import Link from "next/link"; import Image from "next/image"; import logo from "../logo.svg"; -const Header: React.FC<{ page: "index" | "semantic" }> = ({ page }) => { +const Header: React.FC<{ page: "index" | "semantic" | "playground" }> = ({ + page, +}) => { const sidebar = page === "index" ? ( diff --git a/web/src/components/searchbox.tsx b/web/src/components/searchbox.tsx index ed1733b..1cc422f 100644 --- a/web/src/components/searchbox.tsx +++ b/web/src/components/searchbox.tsx @@ -35,7 +35,8 @@ const SearchBoxInternal: React.FC<{ disable: () => void, enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void ) => void; -}> = ({ search }) => { + onQuery?: (q: string) => any; +}> = ({ search, onQuery }) => { const initial_query = initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || ""; @@ -107,7 +108,10 @@ const SearchBoxInternal: React.FC<{ className="flex-1 resize-none border border-gray-300 px-1" ref={inputRef} value={query} - onChange={(e) => setQuery(e.target.value)} + onChange={(e) => { + setQuery(e.target.value); + onQuery && onQuery(e.target.value); + }} onKeyDown={(e) => { // if , blur the input box if (e.key === "Escape") e.currentTarget.blur(); diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index e34d029..41482d1 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -38,7 +38,7 @@ const Home: NextPage = () => { welcomed. - + ); }; diff --git a/web/src/pages/playground.tsx b/web/src/pages/playground.tsx new file mode 100644 index 0000000..fd00fff --- /dev/null +++ b/web/src/pages/playground.tsx @@ -0,0 +1,315 @@ +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"; + +const MAX_FOLLOWUPS = 4; +const DEFAULT_PROMPTS = { + source: { + prefix: + "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", + suffix: + "\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.", + }, + 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 " + + 'cite all of them. For example: "AGI is concerning [c, d, e]."\n\n', + modes: { + 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", + 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", + }, +}; +const DEFAULT_SETTINGS = { + prompts: DEFAULT_PROMPTS, + mode: "default" as Mode, + completions: "gpt-3.5-turbo", + encoder: "cl100k_base", + topKBlocks: 10, // the number of blocks to use as citations + numTokens: 4095, + tokensBuffer: 50, // the number of tokens to leave as a buffer when calculating remaining tokens + 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 COMPLETION_MODELS = ["gpt-3.5-turbo", "gpt-4"]; +const ENCODERS = ["cl100k_base"]; + +const updateIn = (obj, [head, ...rest]: string[], val: any) => { + if (!head) { + // No path provided - do nothing + } else if (!rest || rest.length == 0) { + obj[head] = val; + } else { + updateIn(obj[head], rest, val); + } + return obj; +}; + +type ChatSettingsParams = { + settings: LLMSettings; + updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void; +}; + +const ChatSettings = ({ settings, updateSettings }: ChatSettingsParams) => { + const update = (setting: string) => (event: ChangeEvent) => { + updateSettings((prev) => ({ + ...prev, + [setting]: (event.target as HTMLInputElement).value, + })); + }; + const between = + (setting: string, min?: number, max?: number, parser?) => + (event: ChangeEvent) => { + let num = parser((event.target as HTMLInputElement).value); + if (isNaN(num)) { + return; + } else if (min !== undefined && num < min) { + num = min; + } else if (max !== undefined && num > max) { + num = max; + } + updateSettings((prev) => ({ ...prev, [setting]: num })); + }; + const intBetween = (setting: string, min?: number, max?: number) => + between(setting, min, max, (v: any) => parseInt(v, 10)); + const floatBetween = (setting: string, min?: number, max?: number) => + between(setting, min, max, parseFloat); + return ( +
    +

    Models

    +
    + + +
    + +
    + + +
    + +

    Token options

    +
    + + +
    + +
    + + +
    + +

    Prompt options

    +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + ); +}; + +type ChatPromptParams = { + settings: LLMSettings; + query: string; + history: Entry[]; + updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void; +}; + +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 })); + }; + return ( +
    +
    + Source prompt + +
    (This is where sources will be injected)
    + {history.length > 0 && ( + + )} +
    + {history.length > 0 && ( +
    + History + {history.map((entry) => ( +
    {entry.content}
    + ))} +
    + )} +
    + Question prompt + + +
    +
    Q: {query}
    +
    + ); +}; + +const Playground: NextPage = () => { + const [sessionId, setSessionId] = useState(""); + const [settings, updateSettings] = useState(DEFAULT_SETTINGS); + + const [query, setQuery] = useState(""); + const [history, setHistory] = useState([]); + + const setMode = (mode: [Mode, boolean]) => { + if (mode[1]) { + 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()); + }, []); + + return ( + <> + + AI Safety Info + +
    +
    + +
    + + + +
    +
    + + ); +}; + +export default Playground; diff --git a/web/src/types.ts b/web/src/types.ts index 9bfede9..f78374e 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -44,3 +44,17 @@ export type SearchResult = { export type CurrentSearch = (AssistantEntry & { phase?: string }) | undefined; 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; + historyFraction?: number; + contextFraction?: number; +};