Merge branch 'joriskalz-Persona-From-Text'. Fixes #282

This commit is contained in:
Enrico Ros
2023-12-21 04:31:21 -08:00
6 changed files with 195 additions and 137 deletions
@@ -3,19 +3,20 @@ import { shallow } from 'zustand/shallow';
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ScienceIcon from '@mui/icons-material/Science';
import SearchIcon from '@mui/icons-material/Search';
import TelegramIcon from '@mui/icons-material/Telegram';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { Link } from '~/common/components/Link';
import { navigateToPersonas } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { usePurposeStore } from './store-purposes';
// 'special' purpose IDs, for tile hiding purposes
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
// Constants for tile sizes / grid width - breakpoints need to be computed here to work around
// the "flex box cannot shrink over wrapped content" issue
//
@@ -47,7 +48,6 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
// external state
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator);
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return {
@@ -113,6 +113,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
@@ -156,10 +158,14 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
<Button
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
onClick={() => !editMode && handlePurposeChanged(spId as SystemPurposeId)}
onClick={() => editMode
? toggleHiddenPurposeId(spId)
: handlePurposeChanged(spId as SystemPurposeId)
}
sx={{
flexDirection: 'column',
fontWeight: 500,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
@@ -171,9 +177,10 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
>
{editMode && (
<Checkbox
label={<Typography level='body-sm'>show</Typography>}
checked={!hiddenPurposeIDs.includes(spId)} onChange={() => toggleHiddenPurposeId(spId)}
sx={{ alignSelf: 'flex-start' }}
color='neutral'
checked={!hiddenPurposeIDs.includes(spId)}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div style={{ fontSize: '2rem' }}>
@@ -185,28 +192,43 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
</Button>
</Grid>
))}
{/* Button to start the YouTube persona creator */}
{labsPersonaYTCreator && <Grid>
{/* Button to start the Persona Creator */}
{(editMode || !hidePersonaCreator) && <Grid>
<Button
variant='soft' color='neutral'
component={Link} noLinkStyle href='/personas'
onClick={() => editMode
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
: void navigateToPersonas()
}
sx={{
'--Icon-fontSize': '2rem',
flexDirection: 'column',
fontWeight: 500,
// gap: bpTileGap,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
border: `1px dashed`,
boxShadow: 'md',
backgroundColor: 'background.surface',
// border: `1px dashed`,
// borderColor: 'neutral.softActiveBg',
boxShadow: 'xs',
backgroundColor: 'neutral.softDisabledBg',
}}
>
{editMode && (
<Checkbox
color='neutral'
checked={!hidePersonaCreator}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div>
<ScienceIcon />
<div style={{ fontSize: '2rem' }}>
🎭
</div>
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
</div>
<div>
YouTube persona creator
<div style={{ textAlign: 'center' }}>
Persona Creator
</div>
</Button>
</Grid>}
+3 -10
View File
@@ -2,7 +2,7 @@ import * as React from 'react';
import { Box, Container, ListDivider, Sheet, Typography } from '@mui/joy';
import { YTPersonaCreator } from './YTPersonaCreator';
import { PersonaCreator } from './PersonaCreator';
import ScienceIcon from '@mui/icons-material/Science';
@@ -18,19 +18,12 @@ export function AppPersonas() {
<Container disableGutters maxWidth='md' sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography level='title-lg' sx={{ textAlign: 'center' }}>
Advanced AI Personas
AI Personas Creator
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Typography>
Experimental
</Typography>
<ScienceIcon color='primary' />
</Box>
<ListDivider sx={{ my: 2 }} />
<YTPersonaCreator />
<PersonaCreator />
</Container>
@@ -1,11 +1,13 @@
import * as React from 'react';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, IconButton, Input, LinearProgress, Tooltip, Typography } from '@mui/joy';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, Input, LinearProgress, Tab, TabList, TabPanel, Tabs, Textarea, Typography } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import WhatshotIcon from '@mui/icons-material/Whatshot';
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { GoodModal } from '~/common/components/GoodModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { apiQuery } from '~/common/util/trpc.client';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
@@ -37,9 +39,9 @@ function useTranscriptFromVideo(videoID: string | null) {
}
const YouTubePersonaSteps: LLMChainStep[] = [
const PersonaCreationSteps: LLMChainStep[] = [
{
name: 'Analyzing the transcript',
name: 'Analyzing the transcript / text',
setSystem: 'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
addUserInput: true,
addUser: 'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
@@ -62,14 +64,15 @@ const YouTubePersonaSteps: LLMChainStep[] = [
];
export function YTPersonaCreator() {
export function PersonaCreator() {
// state
const [videoURL, setVideoURL] = React.useState('');
const [videoID, setVideoID] = React.useState('');
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
const [personaText, setPersonaText] = React.useState('');
// external state
const [diagramLlm, llmComponent] = useFormRadioLlmType();
const [personaLlm, llmComponent] = useFormRadioLlmType('Persona Creation Model');
// fetch transcript when the Video ID is ready, then store it
const { transcript, thumbnailUrl, title, isFetching, isError, error: transcriptError } =
@@ -78,7 +81,7 @@ export function YTPersonaCreator() {
// use the transformation sequence to create a persona
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
useLLMChain(YouTubePersonaSteps, diagramLlm?.id, personaTranscript ?? undefined);
useLLMChain(PersonaCreationSteps, personaLlm?.id, personaTranscript ?? undefined);
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
@@ -93,61 +96,87 @@ export function YTPersonaCreator() {
}
};
// New handler for persona text change
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPersonaText(e.target.value);
};
return <>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
<YouTubeIcon sx={{ color: '#f00' }} />
<Typography level='title-lg'>
YouTube -&gt; AI persona
</Typography>
</Box>
<Typography level='title-sm' mb={3}>
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
</Typography>
<form onSubmit={handleFetchTranscript}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
<Input
required
type='url'
fullWidth
<Tabs defaultValue={0} variant='outlined'>
<TabList sx={{ minHeight: 48 }}>
<Tab>From YouTube Video</Tab>
<Tab>From Text</Tab>
</TabList>
{/* YouTube URL inputs */}
<TabPanel value={0} sx={{ p: 3 }}>
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
YouTube -&gt; Persona
</Typography>
<form onSubmit={handleFetchTranscript}>
<Input
required
type='url'
fullWidth
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL}
onChange={handleVideoIdChange}
sx={{ mb: 1.5 }}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button type='submit' variant='solid' disabled={isFetching || isTransforming || !videoURL} loading={isFetching} sx={{ minWidth: 140 }}>
Create
</Button>
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
Example
</Button>
</GoodTooltip>
</Box>
</form>
</TabPanel>
{/* Text area for users to paste copied text */}
<TabPanel value={1} sx={{ p: 3 }}>
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
<b>Text</b> -&gt; Persona
</Typography>
<Textarea
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL} onChange={handleVideoIdChange}
endDecorator={
<IconButton
variant='outlined' color='neutral'
onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}
>
<WhatshotIcon />
</IconButton>
}
minRows={4} maxRows={8}
placeholder='Paste your text here...'
value={personaText}
onChange={handlePersonaTextChange}
sx={{
backgroundColor: 'background.level1',
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
mb: 1.5,
}}
/>
<Button
type='submit'
variant='solid' disabled={isFetching || isTransforming} loading={isFetching}
sx={{ minWidth: 120 }}>
Create
</Button>
</Box>
</form>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setPersonaTranscript(personaText)} sx={{ minWidth: 140 }}>
Create
</Button>
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
</Box>
</TabPanel>
</Tabs>
{/* LLM selector (chat vs fast) */}
{!isTransforming && !isFinished && llmComponent}
{/* 1. Transcript*/}
{personaTranscript && (
<Card sx={{ mt: 2, boxShadow: 'md' }}>
<CardContent>
<Typography level='title-md' sx={{ mb: 1 }}>
{title || 'Transcript'}
</Typography>
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Image' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
</Card>
)}
{!isTransforming && !isFinished && <Box sx={{ mt: 3 }}>{llmComponent}</Box>}
{/* Errors */}
{isError && (
@@ -161,49 +190,64 @@ export function YTPersonaCreator() {
</Alert>
)}
{/* Persona! */}
{chainOutput && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
YouTuber Persona System Prompt
</Typography>
<Card sx={{ boxShadow: 'md' }}>
<CardContent sx={{
position: 'relative',
'&:hover > button': { opacity: 1 },
}}>
{chainOutput && <>
<Card sx={{ boxShadow: 'md', mt: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
Persona Prompt
</Typography>
<GoodTooltip title='Copy system prompt'>
<Button color='success' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
Copy
</Button>
</GoodTooltip>
</Box>
<CardContent>
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
You can now copy the following text and use it as Custom prompt!
You may now copy the text below and use it as Custom prompt!
</Alert>
<Tooltip title='Copy system prompt' variant='solid'>
<IconButton
variant='outlined' color='neutral' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')}
sx={{
position: 'absolute', right: 0, zIndex: 10,
// opacity: 0, transition: 'opacity 0.3s',
}}>
<ContentCopyIcon />
</IconButton>
</Tooltip>
<Typography level='body-sm'>
<Typography level='title-sm' sx={{ lineHeight: 1.75 }}>
{chainOutput}
</Typography>
</CardContent>
</Card>
</Box>}
</>}
{/* Input: Transcript*/}
{personaTranscript && <>
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
Input Data
</Typography>
<Card>
<CardContent>
<Typography level='title-md' sx={{ mb: 1 }}>
{title || 'Transcript / Text'}
</Typography>
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Thumbnail' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
</Card>
</>}
{/* Intermediate outputs rendered as cards in a grid */}
{chainIntermediates && chainIntermediates.length > 0 && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
{chainIntermediates && chainIntermediates.length > 0 && <>
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
{isTransforming ? 'Working...' : 'Intermediate Work'}
</Typography>
<Grid container spacing={2}>
{chainIntermediates.map((intermediate, i) =>
<Grid xs={12} sm={6} md={4} key={i}>
<Card>
<Card sx={{ height: '100%' }}>
<CardContent>
<Typography level='title-sm' sx={{ mb: 1 }}>
{i + 1}. {YouTubePersonaSteps[i].name}
{i + 1}. {PersonaCreationSteps[i].name}
</Typography>
<Typography level='body-sm'>
{intermediate?.slice(0, 140)}...
@@ -213,27 +257,35 @@ export function YTPersonaCreator() {
</Grid>,
)}
</Grid>
</Box>}
</>}
{/* Embodiment Progress */}
{/* Dialog: Embodiment Progress */}
{isTransforming && <GoodModal open>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
</Box>
<Typography color='success' level='title-lg' sx={{ mt: 1 }}>
Embodying Persona ...
</Typography>
<Typography color='success' level='title-sm' sx={{ mt: 1, fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1, mb: 2 }} />
<Box>
<Typography color='success' level='title-lg'>
Embodying Persona ...
</Typography>
<Typography level='title-sm' sx={{ mt: 1 }}>
Using: {personaLlm?.label}
</Typography>
</Box>
<Box>
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
</Box>
<Typography level='title-sm'>
This may take 1-2 minutes. Do not close this window or the progress will be lost.
If you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
While larger models will produce higher quality prompts,
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
please try again with faster/smaller models.
</Typography>
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 5 }}>
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
Cancel
</Button>
</GoodModal>}
+3 -9
View File
@@ -5,7 +5,6 @@ import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
import CallIcon from '@mui/icons-material/Call';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
@@ -19,17 +18,12 @@ export function UxLabsSettings() {
// external state
const isMobile = useIsMobile();
const {
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator, setLabsSplitBranching,
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsSplitBranching,
} = useUXLabsStore();
return <>
<FormSwitchControl
title={<><YouTubeIcon color={labsPersonaYTCreator ? 'primary' : undefined} sx={{ mr: 0.25 }} /> YouTube Personas</>} description={labsPersonaYTCreator ? 'Creator Enabled' : 'Disabled'}
checked={labsPersonaYTCreator} onChange={setLabsPersonaYTCreator}
/>
<FormSwitchControl
title={<><FormatPaintIcon color={labsMagicDraw ? 'primary' : undefined} sx={{ mr: 0.25 }} />Assisted Draw</>} description={labsMagicDraw ? 'Enabled' : 'Disabled'}
checked={labsMagicDraw} onChange={setLabsMagicDraw}
@@ -58,7 +52,7 @@ export function UxLabsSettings() {
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<FormLabelStart title='Graduated' />
<Typography level='body-xs'>
<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link> · <Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
</Typography>
</FormControl>
+3
View File
@@ -14,6 +14,7 @@ export const ROUTE_INDEX = '/';
export const ROUTE_APP_CHAT = '/';
export const ROUTE_APP_LINK_CHAT = '/link/chat/:linkId';
export const ROUTE_APP_NEWS = '/news';
export const ROUTE_APP_PERSONAS = '/personas';
const ROUTE_CALLBACK_OPENROUTER = '/link/callback_openrouter';
@@ -55,6 +56,8 @@ export const navigateToChat = async (conversationId?: DConversationId) => {
};
export const navigateToNews = navigateFn(ROUTE_APP_NEWS);
export const navigateToPersonas = navigateFn(ROUTE_APP_PERSONAS);
export const navigateBack = Router.back;
export const reloadPage = () => isBrowser && window.location.reload();
-6
View File
@@ -24,9 +24,6 @@ interface UXLabsStore {
labsMagicDraw: boolean;
setLabsMagicDraw: (labsMagicDraw: boolean) => void;
labsPersonaYTCreator: boolean;
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => void;
labsSplitBranching: boolean;
setLabsSplitBranching: (labsSplitBranching: boolean) => void;
@@ -48,9 +45,6 @@ export const useUXLabsStore = create<UXLabsStore>()(
labsMagicDraw: false,
setLabsMagicDraw: (labsMagicDraw: boolean) => set({ labsMagicDraw }),
labsPersonaYTCreator: true, // NOTE: default to true, as it is a graduated experiment
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => set({ labsPersonaYTCreator }),
labsSplitBranching: false,
setLabsSplitBranching: (labsSplitBranching: boolean) => set({ labsSplitBranching }),