1
0
Fork 0

AIStory: swipes

This commit is contained in:
Pabloader 2024-11-01 15:28:26 +00:00
parent 2a9f9d1979
commit fa77bb2339
7 changed files with 213 additions and 70 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -36,6 +36,7 @@ textarea {
width: 100%;
scrollbar-width: thin;
scrollbar-color: var(--color) transparent;
padding: 4px;
}
body {
@ -97,16 +98,21 @@ body {
&.role-user {
background-color: var(--shadeColor);
:not(.last-user) .content .text {
&:not(.last-user) .content .text {
opacity: 0.5;
font-size: 12px;
}
}
&.role-assistant {
border-top: 1px solid var(--backgroundColorDark);
}
>.content {
white-space: pre-wrap;
line-height: 1.5;
display: flex;
flex-direction: row;
flex-direction: column;
width: 100%;
gap: 8px;
@ -116,7 +122,7 @@ body {
min-height: 100px;
height: unset;
resize: vertical;
line-height: 1.25;
line-height: 1.5;
padding: 5px;
border-radius: 3px;
}
@ -124,6 +130,7 @@ body {
>.text {
flex-grow: 1;
width: 100%;
animation-duration: 300ms;
>.bold {
font-weight: bold;
@ -141,7 +148,9 @@ body {
>.buttons {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 8px;
>.icon {
@ -158,6 +167,19 @@ body {
font-family: var(--emojiColorFont);
}
}
>.swipes {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
gap: 8px;
>div {
cursor: pointer;
font-size: 20px;
}
}
}
}
}
@ -173,3 +195,26 @@ body {
}
}
}
@keyframes swipe-from-left {
0% {
position: relative;
left: -100%;
}
100% {
position: relative;
left: 0;
}
}
@keyframes swipe-from-right {
0% {
position: relative;
right: -100%;
}
100% {
position: relative;
right: 0;
}
}

View File

@ -8,8 +8,9 @@ export const Chat = () => {
const lastMessage = messages.at(-1);
const lastMessageSwipe = lastMessage?.swipes[lastMessage.currentSwipe];
const lastMessageContent = lastMessageSwipe?.displayContent ?? lastMessageSwipe.content;
const lastMessageContent = lastMessageSwipe?.displayContent ?? lastMessageSwipe?.content;
const lastUserId = messages.findLastIndex(m => m.role === 'user');
const lastAssistantId = messages.findLastIndex(m => m.role === 'assistant');
useEffect(() => {
if (chatRef.current) {
@ -23,7 +24,11 @@ export const Chat = () => {
return (
<div class="chat" ref={chatRef}>
{messages.map((m, i) => (
<Message message={m} key={i} index={i} isLastUser={i === lastUserId}/>
<Message
message={m}
key={i} index={i}
isLastUser={i === lastUserId} isLastAssistant={i === lastAssistantId}
/>
))}
</div>
);

View File

@ -6,17 +6,19 @@ interface IProps {
message: IMessage;
index: number;
isLastUser: boolean;
isLastAssistant: boolean;
}
export const Message = ({ message, index, isLastUser }: IProps) => {
const { editMessage, deleteMessage } = useContext(GlobalContext);
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
const { editMessage, deleteMessage, setCurrentSwipe } = useContext(GlobalContext);
const [editing, setEditing] = useState(false);
const [savedMessage, setSavedMessage] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const swipe = useMemo(() => message.swipes[message.currentSwipe], [message.swipes, message.currentSwipe]);
const content = useMemo(() => swipe.displayContent ?? swipe.content, [swipe]);
const htmlContent = useMemo(() => formatMessage(content), [content]);
const content = useMemo(() => swipe?.displayContent ?? swipe?.content, [swipe]);
const htmlContent = useMemo(() => formatMessage(content ?? ''), [content]);
const handleToggleEdit = useCallback(() => {
setEditing(!editing);
@ -44,6 +46,22 @@ export const Message = ({ message, index, isLastUser }: IProps) => {
}
}, [editMessage, index]);
const handleSwipeLeft = useCallback(() => {
setCurrentSwipe(index, message.currentSwipe - 1);
if (textRef.current) {
textRef.current.style.animationName = '';
textRef.current.style.animationName = 'swipe-from-left';
}
}, [setCurrentSwipe, index, message]);
const handleSwipeRight = useCallback(() => {
setCurrentSwipe(index, message.currentSwipe + 1);
if (textRef.current) {
textRef.current.style.animationName = '';
textRef.current.style.animationName = 'swipe-from-right';
}
}, [setCurrentSwipe, index, message]);
useEffect(() => {
if (textareaRef.current) {
const area = textareaRef.current;
@ -57,20 +75,29 @@ export const Message = ({ message, index, isLastUser }: IProps) => {
<div class="content">
{editing
? <textarea onInput={handleEdit} value={content} class="edit-input" ref={textareaRef} />
: <div class="text" dangerouslySetInnerHTML={{ __html: htmlContent }} />
: <div class="text" dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef}/>
}
{(isLastUser || message.role === 'assistant') &&
<div class="buttons">
{editing
? <>
<button class="icon" onClick={handleToggleEdit}></button>
<button class="icon" onClick={handleCancelEdit}></button>
<button class="icon" onClick={handleDeleteMessage}>🗑</button>
<button class="icon" onClick={handleCancelEdit}></button>
</>
: <>
{isLastAssistant &&
<div class="swipes">
<div onClick={handleSwipeLeft}></div>
<div>{message.currentSwipe + 1}/{message.swipes.length}</div>
<div onClick={handleSwipeRight}></div>
</div>
}
<button class="icon" onClick={handleToggleEdit}>🖊</button>
</>
}
</div>
}
</div>
</div>
);

View File

@ -14,7 +14,7 @@ export const WORLD_INFO = p`
### Ether kinetics
In this world, people refer to what others might call magic as ether kinetics - the special ability of living beings to consciously manipulate a specific energy known as ether, causing various effects. For simplicity, ether kinetics is often referred to as eth. To use it, person should construct an imaginary structure, called an ether circuit or ether weaving, using their mind's eye to visualize slightly glowing translucent paths in the air, which represent ether channels. Once the desired pattern is established, the user fills it with energy. Ether itself is invisible and intangible; nobody could see it or touch it directly. Ether kinetics is not magic, it is pure physics: you could apply force (e.g. moving the thing around) or pressure (e.g. compressing a gas) to a matter, no more than that. But in creative hands it's potential is endless! For example, if you compress air hard enough - it becomes hot and could set thing on fire. Or if you make air to move in one direction it makes a wind. But eth could be applied to anything material, not just air or other gases: levitating objects or moving liquids around also possible.
In this world, people refer to what others might call magic as ether kinetics - the special ability of living beings to consciously manipulate a specific energy known as ether, causing various effects. For simplicity, ether kinetics is often referred to as eth. To use it, person should construct an imaginary structure, called an ether circuit or ether weaving, using their mind's eye to visualize slightly glowing translucent paths in the air, which represent ether channels. Once the desired pattern is established, the user fills it with energy. Ether itself is invisible and intangible; nobody could see it or touch it directly. Ether kinetics is not magic, it is pure physics: you could apply force (e.g. moving the thing around) or pressure (e.g. compressing a gas) to a matter, no more than that. But in creative hands it's potential is endless! For example, if you compress air hard enough - it becomes hot and could set thing on fire. Or if you make air to move in one direction it makes a wind. But eth could be applied to anything material, not just air or other gases: levitating objects or moving liquids around also possible. Objects could not contain ether or be infused with it.
Eth users are called etherkins, or etherkin in singular form.
Fess does not have any supernatural activity: souls, ghosts or similar mystical creatures are impossible by the laws of the world.
@ -24,6 +24,9 @@ export const WORLD_INFO = p`
- Eth is short name for ether kinetics.
- Etherkin is the person who uses ether kinetics.
- No supernatural activity.
- Pretty weak, not capable of manipulating the reality.
- It has absolutely no influence on minds and could not control them.
- Objects could not be infused with it, nor contain it in any form.
### Races
@ -54,9 +57,13 @@ export const WORLD_INFO = p`
**Median Ridge**:
Towering majestically above the landscape, the Median Ridge is a colossal mountain range that stretches across the east of the continent like a jagged spine. Its towering peaks, shrouded in perpetual snow and ice, create an imposing barrier that divides the land into two distinct halves. The rugged terrain and extreme weather conditions make traversing the ridge a daunting task, limiting communication and trade between the isolated communities on the either sides of the mountains.
### History
Fess was created about 1500 years ago by Maya. It was created in the same geological and cultural state as it is now, not accounting for minor changes. People of Fess speak the same common language that was given to them upon creation.
### Maya
Maya created the world of Fess around 1500 years ago as an imaginative playground, imbuing it with a rich history, diverse races, and an ether kinesis system instead of magic. As the world's creator, Maya has ultimate control over the realm and its inhabitants, but she doesn't think of herself as a traditional goddess figure. Her existence is widely accepted and acknowledged among Fess's inhabitants, who view her as a beloved and protective creator. She's living in that same world in a human form.
Maya created the world of Fess around 1500 years ago as an imaginative playground, imbuing it with a rich history, diverse races, and an ether kinesis system. As the world's creator, Maya has ultimate control over the realm and its inhabitants, but she doesn't think of herself as a traditional goddess figure. Her existence is widely accepted and acknowledged among Fess's inhabitants, who view her as a beloved and protective creator. She's living in that same world in a human form.
Maya looks like a neka with a pair of wolf ears, black hair and a tail with white tips. On her right hand exists an intricate geometric tattoo, resembling an ether circuit with interwoven lines of varying thickness, that stretches from her palm all the way up to her shoulder, glowing with a vibrant cyan color. Her eyes are a deep blue. She wears a futuristic sleeveless bodysuit and a pair of similar looking pants. Maya is very self-assured and enjoys embarrassing villains. She has no weapons, because she doesn't need them.
Nobody recognizes her in her human form and treats just as strange clothed neka, though.
Her intentions in this world is simple: to enjoy food, to learn something new, maybe kick some asses of bad guys, and to get fun in general.
@ -67,11 +74,14 @@ export const START_PROMPT = p`
Write a novel using information above as a reference. Make sure to follow the lore exactly and avoid cliffhangers.
`;
export const CONTINUE_PROPMT = (prompt: string, isStart = false) => p`
This is a description of how you should ${isStart ? 'start' : 'continue'} this story: ${prompt}
export const CONTINUE_PROPMT = (prompt?: string, isStart = false) =>
prompt?.trim()
? p`
This is a description of how you should ${isStart ? 'start' : 'continue'} this story: ${prompt.trim()}
Remember that this story should be infinite and go forever. Avoid cliffhangers and pauses.
`;
`
: `Continue the story forward. Avoid cliffhangers and pauses.`;
export const LLAMA_TEMPLATE = `{% for message in messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}`;

View File

@ -1,10 +1,11 @@
import { createContext } from "preact";
import { useEffect, useMemo, useState } from "preact/hooks";
import { compilePrompt, type IMessage } from "./messages";
import { compilePrompt, trimSentence, type IMessage } from "./messages";
import { LLM } from "./llm";
import { loadContext, saveContext, type IContext } from "./globalConfig";
import messageSound from './assets/message.mp3';
import { CONTINUE_PROPMT } from "./const";
export interface IActions {
setConnectionUrl: (url: string) => void;
@ -13,8 +14,9 @@ export interface IActions {
setMessages: (messages: IMessage[]) => void;
addMessage: (content: string, role: IMessage['role'], triggerNext?: boolean) => void;
editMessage: (index: number, content: string) => void;
editMessage: (index: number, content: string, triggerNext?: boolean) => void;
deleteMessage: (index: number) => void;
setCurrentSwipe: (index: number, swipe: number) => void;
continueMessage: () => void;
}
@ -44,7 +46,8 @@ export const GlobalContextProvider = ({ children }: { children?: any }) => {
]);
setTriggerNext(triggerNext);
},
editMessage: (index, content) => setMessages(messages => (
editMessage: (index, content, triggerNext = false) => {
setMessages(messages =>
messages.map(
(m, i) => ({
...m,
@ -53,49 +56,78 @@ export const GlobalContextProvider = ({ children }: { children?: any }) => {
: m.swipes
})
)
)),
);
setTriggerNext(triggerNext);
},
deleteMessage: (index) => setMessages(messages =>
messages.filter((_, i) => i !== index)
),
setCurrentSwipe: (index, currentSwipe) => {
let shouldTrigger = false;
setMessages(messages =>
messages.map(
(message, i) => {
if (i === index) {
const swipes = message.swipes.slice();
const latestSwipe = swipes.at(-1);
if (currentSwipe >= swipes.length) {
if (latestSwipe.content.length > 0) {
currentSwipe = swipes.length;
swipes.push({ content: '' });
} else {
currentSwipe = swipes.length - 1;
}
shouldTrigger = true;
} else while (currentSwipe < 0) {
currentSwipe += swipes.length;
}
return {
...message, swipes, currentSwipe
};
} else {
return message;
}
}
)
);
setTriggerNext(shouldTrigger);
},
continueMessage: () => setTriggerNext(true),
}), []);
useEffect(() => void (async () => {
if (triggerNext) {
setTriggerNext(false);
const lastMessage = messages.at(-1);
const isContinue = lastMessage?.role === 'assistant';
const promptMessages = messages.slice();
const lastMessage = promptMessages.at(-1);
const isAssistantLast = lastMessage?.role === 'assistant';
const isRegen = isAssistantLast && !lastMessage?.swipes[lastMessage.currentSwipe].content;
const isContinue = isAssistantLast && !isRegen;
let promptMessages = isContinue ? messages.slice(0, -1) : messages;
let messageId = promptMessages.length - 1;
let text: string = '';
let prompt = await compilePrompt(promptMessages);
let messageId: number;
let text: string;
let generatedLength = 0;
if (isContinue) {
messageId = messages.length - 1;
text = lastMessage?.swipes[lastMessage.currentSwipe].content;
prompt += text;
} else {
messageId = messages.length;
text = '';
actions.addMessage('', 'assistant');
promptMessages.push({ role: 'user', currentSwipe: 0, swipes: [{ content: CONTINUE_PROPMT() }] });
}
for (let attempt = 0; attempt < 2; attempt++) {
const prompt = await compilePrompt(promptMessages, { rawUser: isContinue });
if (!isRegen) {
actions.addMessage('', 'assistant');
messageId++;
}
for await (const chunk of LLM.generate(prompt)) {
text += chunk;
generatedLength += chunk.trim().length;
actions.editMessage(messageId, text);
}
if (generatedLength > 100) {
break;
} else {
text = text.trim() + '\n\n';
promptMessages.push({ role: 'assistant', currentSwipe: 0, swipes: [{ content: text.trim() }] });
promptMessages.push({ role: 'user', currentSwipe: 0, swipes: [{ content: '(continue)' }] });
prompt = await compilePrompt(promptMessages, {rawUser: true});
}
}
text = trimSentence(text);
actions.editMessage(messageId, text);
messageSound.currentTime = 0;
messageSound.play();
}

View File

@ -41,7 +41,7 @@ export const compilePrompt = async (messages: IMessage[], { rawUser }: ICompileA
const lastUserMessage = userMessages.at(-1);
let userPrompt: string | undefined = lastUserMessage?.swipes[lastUserMessage.currentSwipe].content;
if (!rawUser && userPrompt) {
userPrompt = CONTINUE_PROPMT(userPrompt, story.length > 0);
userPrompt = CONTINUE_PROPMT(userPrompt, story.length === 0);
}
const templateMessages: ITemplateMessage[] = [
@ -110,3 +110,27 @@ export const formatMessage = (message: string): string => {
return resultHTML;
}
export const trimSentence = (text: string): string => {
let latestEnd = -1;
let latestPairEnd = text.length;
for (const end of '.!?;…*"`)}]\n') {
latestEnd = Math.max(latestEnd, text.lastIndexOf(end));
}
for (const char of '*"`') {
const idx = text.lastIndexOf(char);
const match = text.match(new RegExp(`[${char}]`, 'g'));
if (match && match.length % 2 !== 0) {
latestPairEnd = Math.min(latestPairEnd, idx - 1);
}
}
latestEnd = Math.min(latestEnd, latestPairEnd);
if (latestEnd > 0) {
text = text.slice(0, latestEnd + 1);
}
return text.trimEnd();
}