AIStory: edit messages
This commit is contained in:
parent
9df2993b38
commit
5bee177d84
|
|
@ -1,11 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>$TITLE$</title>
|
||||
<style>
|
||||
html, body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
|
|
@ -16,7 +22,9 @@
|
|||
</style>
|
||||
$ICON$
|
||||
</head>
|
||||
|
||||
<body>
|
||||
$SCRIPT$
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -5,33 +5,44 @@ const fontPlugin: BunPlugin = {
|
|||
name: "Font loader",
|
||||
async setup(build) {
|
||||
build.onLoad({ filter: /\.font\.css$/ }, async (args) => {
|
||||
const css = await Bun.file(args.path).text();
|
||||
let css = await Bun.file(args.path).text();
|
||||
|
||||
const match = css.match(/url\(['"]?([^'")]*)['"]?\)/);
|
||||
if (match?.[1]) {
|
||||
const fontName = match[1];
|
||||
const fontPath = path.resolve(path.dirname(args.path), fontName);
|
||||
const fontFile = Bun.file(fontPath);
|
||||
if (await fontFile.exists()) {
|
||||
const buffer = Buffer.from(await fontFile.arrayBuffer());
|
||||
const url = `data:;base64,${buffer.toString('base64')}`;
|
||||
const updatedCSS = css.replace(fontName, url)
|
||||
.replace(/(\n\s*)*/g, '')
|
||||
.replace(/;\s*\}/g, '}')
|
||||
.replace(/\s*([{:])\s*/g, '$1');
|
||||
const regex = /url\(['"]?(?!data:)([^'")]*)['"]?\)/;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = regex.exec(css)) != null) {
|
||||
if (match?.[1]) {
|
||||
console.log(match[1]);
|
||||
let buffer: Buffer | null = null;
|
||||
const fontName = match[1];
|
||||
if (fontName.startsWith('http')) {
|
||||
const response = await fetch(fontName);
|
||||
if (response.ok) {
|
||||
buffer = Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
} else {
|
||||
const fontPath = path.resolve(path.dirname(args.path), fontName);
|
||||
const fontFile = Bun.file(fontPath);
|
||||
if (await fontFile.exists()) {
|
||||
buffer = Buffer.from(await fontFile.arrayBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contents: `import { injectStyle } from '__style_helper__';
|
||||
injectStyle(${JSON.stringify(updatedCSS)})`,
|
||||
loader: 'js',
|
||||
if (buffer) {
|
||||
const url = `data:;base64,${buffer.toString('base64')}`;
|
||||
css = css.replace(fontName, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
const updatedCSS = css
|
||||
.replace(/(\n\s*)*/g, '')
|
||||
.replace(/;\s*\}/g, '}')
|
||||
.replace(/\s*([{:])\s*/g, '$1')
|
||||
|
||||
return {
|
||||
contents: '',
|
||||
contents: `import { injectStyle } from '__style_helper__';
|
||||
injectStyle(${JSON.stringify(updatedCSS)})`,
|
||||
loader: 'js',
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
/* [0] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.0.woff2) format('woff2');
|
||||
unicode-range: U+1f1e6-1f1ff;
|
||||
}
|
||||
/* [1] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.1.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2620, U+26a7, U+fe0f, U+1f308, U+1f38c, U+1f3c1, U+1f3f3-1f3f4, U+1f6a9, U+e0062-e0063, U+e0065, U+e0067, U+e006c, U+e006e, U+e0073-e0074, U+e0077, U+e007f;
|
||||
}
|
||||
/* [2] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.2.woff2) format('woff2');
|
||||
unicode-range: U+23, U+2a, U+30-39, U+a9, U+ae, U+200d, U+203c, U+2049, U+20e3, U+2122, U+2139, U+2194-2199, U+21a9-21aa, U+23cf, U+23e9-23ef, U+23f8-23fa, U+24c2, U+25aa-25ab, U+25b6, U+25c0, U+25fb-25fe, U+2611, U+2622-2623, U+2626, U+262a, U+262e-262f, U+2638, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267b, U+267e-267f, U+2695, U+269b-269c, U+26a0, U+26a7, U+26aa-26ab, U+26ce, U+26d4, U+2705, U+2714, U+2716, U+271d, U+2721, U+2733-2734, U+2747, U+274c, U+274e, U+2753-2755, U+2757, U+2764, U+2795-2797, U+27a1, U+27b0, U+27bf, U+2934-2935, U+2b05-2b07, U+2b1b-2b1c, U+2b55, U+3030, U+303d, U+3297, U+3299, U+fe0f, U+1f170-1f171, U+1f17e-1f17f, U+1f18e, U+1f191-1f19a, U+1f201-1f202, U+1f21a, U+1f22f, U+1f232-1f23a, U+1f250-1f251, U+1f310, U+1f3a6, U+1f3b5-1f3b6, U+1f3bc, U+1f3e7, U+1f441, U+1f499-1f49c, U+1f49f-1f4a0, U+1f4a2, U+1f4ac-1f4ad, U+1f4b1-1f4b2, U+1f4b9, U+1f4db, U+1f4f2-1f4f6, U+1f500-1f50a, U+1f515, U+1f518-1f524, U+1f52f-1f53d, U+1f549, U+1f54e, U+1f5a4, U+1f5e8, U+1f5ef, U+1f6ab, U+1f6ad-1f6b1, U+1f6b3, U+1f6b7-1f6bc, U+1f6be, U+1f6c2-1f6c5, U+1f6d0-1f6d1, U+1f6d7, U+1f6dc, U+1f7e0-1f7eb, U+1f7f0, U+1f90d-1f90e, U+1f9e1, U+1fa75-1fa77, U+1faaf;
|
||||
}
|
||||
/* [3] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.3.woff2) format('woff2');
|
||||
unicode-range: U+231a-231b, U+2328, U+23f0-23f3, U+2602, U+260e, U+2692, U+2694, U+2696-2697, U+2699, U+26b0-26b1, U+26cf, U+26d1, U+26d3, U+2702, U+2709, U+270f, U+2712, U+fe0f, U+1f302, U+1f321, U+1f392-1f393, U+1f3a9, U+1f3bd, U+1f3ee, U+1f3f7, U+1f3fa, U+1f451-1f462, U+1f484, U+1f489-1f48a, U+1f48c-1f48e, U+1f4a1, U+1f4a3, U+1f4b0, U+1f4b3-1f4b8, U+1f4bb-1f4da, U+1f4dc-1f4f1, U+1f4ff, U+1f50b-1f514, U+1f516-1f517, U+1f526-1f529, U+1f52c-1f52e, U+1f550-1f567, U+1f56f-1f570, U+1f576, U+1f587, U+1f58a-1f58d, U+1f5a5, U+1f5a8, U+1f5b1-1f5b2, U+1f5c2-1f5c4, U+1f5d1-1f5d3, U+1f5dc-1f5de, U+1f5e1, U+1f5f3, U+1f6aa, U+1f6ac, U+1f6bd, U+1f6bf, U+1f6c1, U+1f6cb, U+1f6cd-1f6cf, U+1f6d2, U+1f6e0-1f6e1, U+1f6f0, U+1f97b-1f97f, U+1f9af, U+1f9ba, U+1f9e2-1f9e6, U+1f9ea-1f9ec, U+1f9ee-1f9f4, U+1f9f7-1f9ff, U+1fa71-1fa74, U+1fa79-1fa7b, U+1fa86, U+1fa91-1fa93, U+1fa96, U+1fa99-1faa0, U+1faa2-1faa7, U+1faaa-1faae;
|
||||
}
|
||||
/* [4] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.4.woff2) format('woff2');
|
||||
unicode-range: U+265f, U+26bd-26be, U+26f3, U+26f8, U+fe0f, U+1f004, U+1f0cf, U+1f380-1f384, U+1f386-1f38b, U+1f38d-1f391, U+1f396-1f397, U+1f399-1f39b, U+1f39e-1f39f, U+1f3a3-1f3a5, U+1f3a7-1f3a9, U+1f3ab-1f3b4, U+1f3b7-1f3bb, U+1f3bd-1f3c0, U+1f3c5-1f3c6, U+1f3c8-1f3c9, U+1f3cf-1f3d3, U+1f3f8-1f3f9, U+1f47e, U+1f4e2, U+1f4f7-1f4fd, U+1f52b, U+1f579, U+1f58c-1f58d, U+1f5bc, U+1f6f7, U+1f6f9, U+1f6fc, U+1f93f, U+1f941, U+1f945, U+1f947-1f94f, U+1f9e7-1f9e9, U+1f9f5-1f9f6, U+1fa70-1fa71, U+1fa80-1fa81, U+1fa83-1fa85, U+1fa87-1fa88, U+1fa94-1fa95, U+1fa97-1fa98, U+1faa1, U+1faa9;
|
||||
}
|
||||
/* [5] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.5.woff2) format('woff2');
|
||||
unicode-range: U+2693, U+26e9-26ea, U+26f1-26f2, U+26f4-26f5, U+26fa, U+26fd, U+2708, U+fe0f, U+1f301, U+1f303, U+1f306-1f307, U+1f309, U+1f310, U+1f3a0-1f3a2, U+1f3aa, U+1f3cd-1f3ce, U+1f3d5, U+1f3d7-1f3db, U+1f3df-1f3e6, U+1f3e8-1f3ed, U+1f3ef-1f3f0, U+1f488, U+1f492, U+1f4ba, U+1f54b-1f54d, U+1f5fa-1f5ff, U+1f680-1f6a2, U+1f6a4-1f6a8, U+1f6b2, U+1f6d1, U+1f6d5-1f6d6, U+1f6dd-1f6df, U+1f6e2-1f6e5, U+1f6e9, U+1f6eb-1f6ec, U+1f6f3-1f6f6, U+1f6f8, U+1f6fa-1f6fb, U+1f9bc-1f9bd, U+1f9ed, U+1f9f3, U+1fa7c;
|
||||
}
|
||||
/* [6] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.6.woff2) format('woff2');
|
||||
unicode-range: U+2615, U+fe0f, U+1f32d-1f330, U+1f336, U+1f33d, U+1f345-1f37f, U+1f382, U+1f52a, U+1f942-1f944, U+1f950-1f96f, U+1f99e, U+1f9aa, U+1f9c0-1f9cb, U+1fad0-1fadb;
|
||||
}
|
||||
/* [7] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.7.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2600-2601, U+2603-2604, U+2614, U+2618, U+26a1, U+26c4-26c5, U+26c8, U+26f0, U+2728, U+2744, U+2b1b, U+2b50, U+fe0f, U+1f300, U+1f304-1f305, U+1f308, U+1f30a-1f30f, U+1f311-1f321, U+1f324-1f32c, U+1f331-1f335, U+1f337-1f33c, U+1f33e-1f344, U+1f3d4, U+1f3d6, U+1f3dc-1f3de, U+1f3f5, U+1f400-1f43f, U+1f490, U+1f4a7, U+1f4ab, U+1f4ae, U+1f525, U+1f54a, U+1f573, U+1f577-1f578, U+1f648-1f64a, U+1f940, U+1f980-1f9ae, U+1f9ba, U+1fa90, U+1faa8, U+1fab0-1fabd, U+1fabf, U+1face-1facf, U+1fae7;
|
||||
}
|
||||
/* [8] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.8.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2640, U+2642, U+2695-2696, U+26f7, U+26f9, U+2708, U+2764, U+fe0f, U+1f33e, U+1f373, U+1f37c, U+1f384-1f385, U+1f393, U+1f3a4, U+1f3a8, U+1f3c2-1f3c4, U+1f3c7, U+1f3ca-1f3cc, U+1f3eb, U+1f3ed, U+1f3fb-1f3ff, U+1f466-1f478, U+1f47c, U+1f481-1f483, U+1f486-1f487, U+1f48b, U+1f48f, U+1f491, U+1f4bb-1f4bc, U+1f527, U+1f52c, U+1f574-1f575, U+1f57a, U+1f645-1f647, U+1f64b, U+1f64d-1f64e, U+1f680, U+1f692, U+1f6a3, U+1f6b4-1f6b6, U+1f6c0, U+1f6cc, U+1f91d, U+1f926, U+1f930-1f931, U+1f934-1f93a, U+1f93c-1f93e, U+1f977, U+1f9af-1f9b3, U+1f9b8-1f9b9, U+1f9bc-1f9bd, U+1f9cc-1f9cf, U+1f9d1-1f9df, U+1fa82, U+1fac3-1fac5;
|
||||
}
|
||||
/* [9] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.9.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+261d, U+2620, U+2639-263a, U+2665, U+270a-270d, U+2728, U+2763-2764, U+2b50, U+fe0f, U+1f31a-1f31f, U+1f32b, U+1f383, U+1f389, U+1f3fb-1f3ff, U+1f440-1f450, U+1f463-1f465, U+1f479-1f47b, U+1f47d-1f480, U+1f485, U+1f48b-1f48c, U+1f493-1f49f, U+1f4a4-1f4a6, U+1f4a8-1f4ab, U+1f4af, U+1f525, U+1f573, U+1f590, U+1f595-1f596, U+1f5a4, U+1f5e3, U+1f600-1f644, U+1f648-1f64a, U+1f64c, U+1f64f, U+1f90c-1f925, U+1f927-1f92f, U+1f932-1f933, U+1f970-1f976, U+1f978-1f97a, U+1f9a0, U+1f9b4-1f9b7, U+1f9bb, U+1f9be-1f9bf, U+1f9d0, U+1f9e0-1f9e1, U+1fa75-1fa79, U+1fac0-1fac2, U+1fae0-1fae6, U+1fae8, U+1faf0-1faf8;
|
||||
}
|
||||
/* [10] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.10.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2194-2195, U+2640, U+2642, U+26d3, U+27a1, U+fe0f, U+1f344, U+1f34b, U+1f3c3, U+1f3fb-1f3ff, U+1f426, U+1f468-1f469, U+1f4a5, U+1f525, U+1f642, U+1f6b6, U+1f7e9, U+1f7eb, U+1f9af, U+1f9bc-1f9bd, U+1f9ce, U+1f9d1-1f9d2;
|
||||
}
|
||||
/* [11] */
|
||||
@font-face {
|
||||
font-family: 'Noto Color Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.11.woff2) format('woff2');
|
||||
unicode-range: U+1fa89, U+1fa8f, U+1fabe, U+1fac6, U+1fadc, U+1fadf, U+1fae9;
|
||||
}
|
||||
/* [0] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.0.woff2) format('woff2');
|
||||
unicode-range: U+1f1e6-1f1ff;
|
||||
}
|
||||
/* [1] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.1.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2620, U+26a7, U+fe0f, U+1f308, U+1f38c, U+1f3c1, U+1f3f3-1f3f4, U+1f6a9, U+e0062-e0063, U+e0065, U+e0067, U+e006c, U+e006e, U+e0073-e0074, U+e0077, U+e007f;
|
||||
}
|
||||
/* [2] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.2.woff2) format('woff2');
|
||||
unicode-range: U+23, U+2a, U+30-39, U+a9, U+ae, U+200d, U+203c, U+2049, U+20e3, U+2122, U+2139, U+2194-2199, U+21a9-21aa, U+23cf, U+23e9-23ef, U+23f8-23fa, U+24c2, U+25aa-25ab, U+25b6, U+25c0, U+25fb-25fe, U+2611, U+2622-2623, U+2626, U+262a, U+262e-262f, U+2638, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267b, U+267e-267f, U+2695, U+269b-269c, U+26a0, U+26a7, U+26aa-26ab, U+26ce, U+26d4, U+2705, U+2714, U+2716, U+271d, U+2721, U+2733-2734, U+2747, U+274c, U+274e, U+2753-2755, U+2757, U+2764, U+2795-2797, U+27a1, U+27b0, U+27bf, U+2934-2935, U+2b05-2b07, U+2b1b-2b1c, U+2b55, U+3030, U+303d, U+3297, U+3299, U+fe0f, U+1f170-1f171, U+1f17e-1f17f, U+1f18e, U+1f191-1f19a, U+1f201-1f202, U+1f21a, U+1f22f, U+1f232-1f23a, U+1f250-1f251, U+1f310, U+1f3a6, U+1f3b5-1f3b6, U+1f3bc, U+1f3e7, U+1f441, U+1f499-1f49c, U+1f49f-1f4a0, U+1f4a2, U+1f4ac-1f4ad, U+1f4b1-1f4b2, U+1f4b9, U+1f4db, U+1f4f2-1f4f6, U+1f500-1f50a, U+1f515, U+1f518-1f524, U+1f52f-1f53d, U+1f549, U+1f54e, U+1f5a4, U+1f5e8, U+1f5ef, U+1f6ab, U+1f6ad-1f6b1, U+1f6b3, U+1f6b7-1f6bc, U+1f6be, U+1f6c2-1f6c5, U+1f6d0-1f6d1, U+1f6d7, U+1f6dc, U+1f7e0-1f7eb, U+1f7f0, U+1f90d-1f90e, U+1f9e1, U+1fa75-1fa77, U+1faaf;
|
||||
}
|
||||
/* [3] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.3.woff2) format('woff2');
|
||||
unicode-range: U+231a-231b, U+2328, U+23f0-23f3, U+2602, U+260e, U+2692, U+2694, U+2696-2697, U+2699, U+26b0-26b1, U+26cf, U+26d1, U+26d3, U+2702, U+2709, U+270f, U+2712, U+fe0f, U+1f302, U+1f321, U+1f392-1f393, U+1f3a9, U+1f3bd, U+1f3ee, U+1f3f7, U+1f3fa, U+1f451-1f462, U+1f484, U+1f489-1f48a, U+1f48c-1f48e, U+1f4a1, U+1f4a3, U+1f4b0, U+1f4b3-1f4b8, U+1f4bb-1f4da, U+1f4dc-1f4f1, U+1f4ff, U+1f50b-1f514, U+1f516-1f517, U+1f526-1f529, U+1f52c-1f52e, U+1f550-1f567, U+1f56f-1f570, U+1f576, U+1f587, U+1f58a-1f58d, U+1f5a5, U+1f5a8, U+1f5b1-1f5b2, U+1f5c2-1f5c4, U+1f5d1-1f5d3, U+1f5dc-1f5de, U+1f5e1, U+1f5f3, U+1f6aa, U+1f6ac, U+1f6bd, U+1f6bf, U+1f6c1, U+1f6cb, U+1f6cd-1f6cf, U+1f6d2, U+1f6e0-1f6e1, U+1f6f0, U+1f97b-1f97f, U+1f9af, U+1f9ba, U+1f9e2-1f9e6, U+1f9ea-1f9ec, U+1f9ee-1f9f4, U+1f9f7-1f9ff, U+1fa71-1fa74, U+1fa79-1fa7b, U+1fa86, U+1fa91-1fa93, U+1fa96, U+1fa99-1faa0, U+1faa2-1faa7, U+1faaa-1faae;
|
||||
}
|
||||
/* [4] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.4.woff2) format('woff2');
|
||||
unicode-range: U+265f, U+26bd-26be, U+26f3, U+26f8, U+fe0f, U+1f004, U+1f0cf, U+1f380-1f384, U+1f386-1f38b, U+1f38d-1f391, U+1f396-1f397, U+1f399-1f39b, U+1f39e-1f39f, U+1f3a3-1f3a5, U+1f3a7-1f3a9, U+1f3ab-1f3b4, U+1f3b7-1f3bb, U+1f3bd-1f3c0, U+1f3c5-1f3c6, U+1f3c8-1f3c9, U+1f3cf-1f3d3, U+1f3f8-1f3f9, U+1f47e, U+1f4e2, U+1f4f7-1f4fd, U+1f52b, U+1f579, U+1f58c-1f58d, U+1f5bc, U+1f6f7, U+1f6f9, U+1f6fc, U+1f93f, U+1f941, U+1f945, U+1f947-1f94f, U+1f9e7-1f9e9, U+1f9f5-1f9f6, U+1fa70-1fa71, U+1fa80-1fa81, U+1fa83-1fa85, U+1fa87-1fa88, U+1fa94-1fa95, U+1fa97-1fa98, U+1faa1, U+1faa9;
|
||||
}
|
||||
/* [5] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.5.woff2) format('woff2');
|
||||
unicode-range: U+2693, U+26e9-26ea, U+26f1-26f2, U+26f4-26f5, U+26fa, U+26fd, U+2708, U+fe0f, U+1f301, U+1f303, U+1f306-1f307, U+1f309, U+1f310, U+1f3a0-1f3a2, U+1f3aa, U+1f3cd-1f3ce, U+1f3d5, U+1f3d7-1f3db, U+1f3df-1f3e6, U+1f3e8-1f3ed, U+1f3ef-1f3f0, U+1f488, U+1f492, U+1f4ba, U+1f54b-1f54d, U+1f5fa-1f5ff, U+1f680-1f6a2, U+1f6a4-1f6a8, U+1f6b2, U+1f6d1, U+1f6d5-1f6d6, U+1f6dd-1f6df, U+1f6e2-1f6e5, U+1f6e9, U+1f6eb-1f6ec, U+1f6f3-1f6f6, U+1f6f8, U+1f6fa-1f6fb, U+1f9bc-1f9bd, U+1f9ed, U+1f9f3, U+1fa7c;
|
||||
}
|
||||
/* [6] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.6.woff2) format('woff2');
|
||||
unicode-range: U+2615, U+fe0f, U+1f32d-1f330, U+1f336, U+1f33d, U+1f345-1f37f, U+1f382, U+1f52a, U+1f942-1f944, U+1f950-1f96f, U+1f99e, U+1f9aa, U+1f9c0-1f9cb, U+1fad0-1fadb;
|
||||
}
|
||||
/* [7] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.7.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2600-2601, U+2603-2604, U+2614, U+2618, U+26a1, U+26c4-26c5, U+26c8, U+26f0, U+2728, U+2744, U+2b1b, U+2b50, U+fe0f, U+1f300, U+1f304-1f305, U+1f308, U+1f30a-1f30f, U+1f311-1f321, U+1f324-1f32c, U+1f331-1f335, U+1f337-1f33c, U+1f33e-1f344, U+1f3d4, U+1f3d6, U+1f3dc-1f3de, U+1f3f5, U+1f400-1f43f, U+1f490, U+1f4a7, U+1f4ab, U+1f4ae, U+1f525, U+1f54a, U+1f573, U+1f577-1f578, U+1f648-1f64a, U+1f940, U+1f980-1f9ae, U+1f9ba, U+1fa90, U+1faa8, U+1fab0-1fabd, U+1fabf, U+1face-1facf, U+1fae7;
|
||||
}
|
||||
/* [8] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.8.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2640, U+2642, U+2695-2696, U+26f7, U+26f9, U+2708, U+2764, U+fe0f, U+1f33e, U+1f373, U+1f37c, U+1f384-1f385, U+1f393, U+1f3a4, U+1f3a8, U+1f3c2-1f3c4, U+1f3c7, U+1f3ca-1f3cc, U+1f3eb, U+1f3ed, U+1f3fb-1f3ff, U+1f466-1f478, U+1f47c, U+1f481-1f483, U+1f486-1f487, U+1f48b, U+1f48f, U+1f491, U+1f4bb-1f4bc, U+1f527, U+1f52c, U+1f574-1f575, U+1f57a, U+1f645-1f647, U+1f64b, U+1f64d-1f64e, U+1f680, U+1f692, U+1f6a3, U+1f6b4-1f6b6, U+1f6c0, U+1f6cc, U+1f91d, U+1f926, U+1f930-1f931, U+1f934-1f93a, U+1f93c-1f93e, U+1f977, U+1f9af-1f9b3, U+1f9b8-1f9b9, U+1f9bc-1f9bd, U+1f9cc-1f9cf, U+1f9d1-1f9df, U+1fa82, U+1fac3-1fac5;
|
||||
}
|
||||
/* [9] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.9.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+261d, U+2620, U+2639-263a, U+2665, U+270a-270d, U+2728, U+2763-2764, U+2b50, U+fe0f, U+1f31a-1f31f, U+1f32b, U+1f383, U+1f389, U+1f3fb-1f3ff, U+1f440-1f450, U+1f463-1f465, U+1f479-1f47b, U+1f47d-1f480, U+1f485, U+1f48b-1f48c, U+1f493-1f49f, U+1f4a4-1f4a6, U+1f4a8-1f4ab, U+1f4af, U+1f525, U+1f573, U+1f590, U+1f595-1f596, U+1f5a4, U+1f5e3, U+1f600-1f644, U+1f648-1f64a, U+1f64c, U+1f64f, U+1f90c-1f925, U+1f927-1f92f, U+1f932-1f933, U+1f970-1f976, U+1f978-1f97a, U+1f9a0, U+1f9b4-1f9b7, U+1f9bb, U+1f9be-1f9bf, U+1f9d0, U+1f9e0-1f9e1, U+1fa75-1fa79, U+1fac0-1fac2, U+1fae0-1fae6, U+1fae8, U+1faf0-1faf8;
|
||||
}
|
||||
/* [10] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.10.woff2) format('woff2');
|
||||
unicode-range: U+200d, U+2194-2195, U+2640, U+2642, U+26d3, U+27a1, U+fe0f, U+1f344, U+1f34b, U+1f3c3, U+1f3fb-1f3ff, U+1f426, U+1f468-1f469, U+1f4a5, U+1f525, U+1f642, U+1f6b6, U+1f7e9, U+1f7eb, U+1f9af, U+1f9bc-1f9bd, U+1f9ce, U+1f9d1-1f9d2;
|
||||
}
|
||||
/* [11] */
|
||||
@font-face {
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/notoemoji/v51/bMrymSyK7YY-MEu6aWjPFMHQUnEOtg_Uy9ZzkQ.11.woff2) format('woff2');
|
||||
unicode-range: U+1fa89, U+1fa8f, U+1fabe, U+1fac6, U+1fadc, U+1fadf, U+1fae9;
|
||||
}
|
||||
body {
|
||||
--google-font-color-notocoloremoji:colrv1;
|
||||
}
|
||||
|
|
@ -1,12 +1,27 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--backgroundColorDark: #111111;
|
||||
--backgroundColor: #333333;
|
||||
--color: #DCDCD2;
|
||||
--italicColor: #AFAFAF;
|
||||
--quoteColor: #D4E5FF;
|
||||
|
||||
--emojiFont: "Noto Emoji", sans-serif;
|
||||
--emojiColorFont: "Noto Color Emoji", sans-serif;
|
||||
}
|
||||
|
||||
textarea {
|
||||
color: var(--color);
|
||||
background-color: var(--backgroundColor);
|
||||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
background-color: transparent;
|
||||
resize: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color) transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -57,13 +72,43 @@ body {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
|
||||
>.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
>.buttons {
|
||||
>.icon {
|
||||
font-family: var(--emojiFont);
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--color);
|
||||
cursor: pointer;
|
||||
|
||||
&.color {
|
||||
font-family: var(--emojiColorFont);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
|
||||
>textarea {
|
||||
background-color: var(--backgroundColorDark);
|
||||
border: 1px solid var(--italicColor);
|
||||
min-height: 100px;
|
||||
height: unset;
|
||||
resize: vertical;
|
||||
line-height: 1.25;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,21 +120,6 @@ body {
|
|||
min-height: 48px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--color);
|
||||
|
||||
>textarea {
|
||||
color: var(--color);
|
||||
background-color: var(--backgroundColor);
|
||||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
background-color: transparent;
|
||||
resize: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color) transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,9 @@ export const Chat = () => {
|
|||
const { messages } = useContext(GlobalContext);
|
||||
const chatRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lastMessageContent = messages.at(-1)?.displayContent ?? messages.at(-1)?.content;
|
||||
const lastMessage = messages.at(-1);
|
||||
const lastMessageSwipe = lastMessage?.swipes[lastMessage.currentSwipe];
|
||||
const lastMessageContent = lastMessageSwipe?.displayContent ?? lastMessageSwipe.content;
|
||||
|
||||
useEffect(() => {
|
||||
if (chatRef.current) {
|
||||
|
|
@ -20,7 +22,7 @@ export const Chat = () => {
|
|||
return (
|
||||
<div class="chat" ref={chatRef}>
|
||||
{messages.map((m, i) => (
|
||||
<Message message={m} key={i} />
|
||||
<Message message={m} key={i} index={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { GlobalContext } from "../context";
|
|||
export const Input = () => {
|
||||
const { input, setInput, addMessage } = useContext(GlobalContext);
|
||||
|
||||
console.log({input});
|
||||
|
||||
const handleChange = useCallback((e: Event) => {
|
||||
if (e.target instanceof HTMLTextAreaElement) {
|
||||
setInput(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,47 @@
|
|||
import { useContext, useMemo } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { IMessage } from "../messages";
|
||||
import { GlobalContext } from "../context";
|
||||
|
||||
interface IProps {
|
||||
message: IMessage;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const Message = ({ message }: IProps) => {
|
||||
const { name } = useContext(GlobalContext);
|
||||
export const Message = ({ message, index }: IProps) => {
|
||||
const { name, editMessage } = useContext(GlobalContext);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [savedMessage, setSavedMessage] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const swipe = useMemo(() => message.swipes[message.currentSwipe], [message.swipes, message.currentSwipe]);
|
||||
const content = useMemo(() => swipe.displayContent ?? swipe.content, [swipe]);
|
||||
|
||||
const handleToggleEdit = useCallback(() => {
|
||||
setEditing(!editing);
|
||||
if (!editing) {
|
||||
setSavedMessage(content);
|
||||
}
|
||||
}, [editing, content]);
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setEditing(false);
|
||||
editMessage(index, savedMessage);
|
||||
}, [editMessage, index, savedMessage]);
|
||||
|
||||
const handleEdit = useCallback((e: InputEvent) => {
|
||||
if (e.target instanceof HTMLTextAreaElement) {
|
||||
const newContent = e.target.value;
|
||||
editMessage(index, newContent);
|
||||
}
|
||||
}, [editMessage, index]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
const area = textareaRef.current;
|
||||
area.style.height = '0'; // reset
|
||||
area.style.height = `${area.scrollHeight + 10}px`;
|
||||
}
|
||||
}, [content, editing]);
|
||||
|
||||
return (
|
||||
<div class="message">
|
||||
|
|
@ -16,10 +50,22 @@ export const Message = ({ message }: IProps) => {
|
|||
{message.role === 'user' ? name : '---'}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
{editing
|
||||
? <>
|
||||
<button class="icon" onClick={handleToggleEdit}>✔</button>
|
||||
<button class="icon" onClick={handleCancelEdit}>❌</button>
|
||||
</>
|
||||
: <>
|
||||
<button class="icon" onClick={handleToggleEdit}>🖊</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{message.displayContent ?? message.content}
|
||||
{editing
|
||||
? <textarea onInput={handleEdit} value={content} class="edit-input" ref={textareaRef} />
|
||||
: content
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
export const SYSTEM_PROMPT = `
|
||||
You are creative and skilled writer.
|
||||
Write a story based on the world description below.
|
||||
Make sure you're following the provided lore exactly and not making up impossible things.
|
||||
`.trim().replace(/[ \r\n]+/ig, ' ');
|
||||
|
||||
export const WORLD_INFO = `
|
||||
### General World description
|
||||
|
||||
Fess is a fictional fantasy world inhabited by diverse creatures and races. Its technological progress is roughly equivalent to Earth's medieval ages, featuring bows and arrows, horses as the primary mode of transport, and fortified cities.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
In summary:
|
||||
- Ether is and invisible and intangible energy.
|
||||
- Ether kinetics is manipulation of ether.
|
||||
- Eth is short name for ether kinetics.
|
||||
- Etherkin is the person who uses ether kinetics.
|
||||
- No supernatural activity.
|
||||
|
||||
### Races
|
||||
|
||||
The people of Fess are divided into three races:
|
||||
|
||||
**Ida**:
|
||||
The vast majority of the population, analogous to Earth's humans. They possess no special abilities or advantages and are average in all respects. Important: don't call them "humans", because word "human" in this world could be applied to any race, including neka and folia. Replace "human" with "ida". They have many kingdoms of different size on the both sides of the Median Ridge.
|
||||
|
||||
**Neka**:
|
||||
Members of this race possess certain animal traits such as animal ears or tails, though not always. They exhibit great physical strength, agility, and stamina. Nearly all of them were wiped out during the racist hunts over a thousand years ago. The surviving population now resides exclusively within Nordille, the largest city of the former neka kingdom. Occasionally, a few individual neka can be found traveling outside their city. Almost all neka are extremely devout towards Maya, mentioning her in nearly every conversation due to her saving their kind from extinction.
|
||||
|
||||
**Folia**:
|
||||
Similar to high elves in popular culture, members of the folia race possess long ears, blonde or white hair, and are skilled in utilizing eth. They are often found dwelling in lush, verdant forests, where their keen senses and natural agility allow them to traverse the wilderness with ease. Folia are renowned for their archery skills, preferring to use bows crafted from the finest woods found in their forest homes. Their keen eyesight and steady hands make them formidable marksmen, capable of hitting targets with precision from great distances. In addition to their combat prowess, folia are also known for their wisdom and deep connection to nature. They often serve as guardians and caretakers of the forests, using their knowledge of plants and animals to maintain the delicate balance of their ecosystems. Folia are generally peaceful and prefer to avoid conflicts, but when threatened, they can be fierce defenders of their homes and loved ones. They have two countries: North and South Folia, both located in the dense forests at the south-west of the continent.
|
||||
|
||||
In a general sense, the words "human" or "people" can apply to any race in this world.
|
||||
|
||||
### Geography
|
||||
|
||||
Alongside Nordille, there are several notable geographical features:
|
||||
|
||||
**Trinity**:
|
||||
Nestled in the middle of the continent, Trinity stands as a beacon of unity and cooperation among the races. This independent city-state is a harmonious blend of neka, ida, and folia cultures, with representatives from each race playing an active role in its governance. The city's architecture reflects this diversity, with neka-inspired stone structures, folia-influenced wooden buildings, and ida-created metalwork all coexisting in a beautiful tapestry of design.
|
||||
Trinity's streets are a vibrant mix of races, with neka and folia merchants hawking their wares alongside human craftsmen. The city's central plaza is home to a grand statue of Maya, symbolizing the unity and cooperation among the races. Here, festivals and celebrations are held throughout the year, bringing the people of Trinity together in a joyous display of their shared humanity.
|
||||
|
||||
**The Eternal Desert**:
|
||||
Stretching across the equator of Fess like a vast, shimmering ribbon, the Eternal Desert is a formidable barrier that has long separated the known world from the mysteries of the southern hemisphere. Its endless dunes of golden sand, scorching heat, and lack of water have made it virtually impassable for centuries, earning it the reputation of an uncrossable wasteland. The few brave souls who have attempted to traverse its treacherous expanse have vanished without a trace, their fates lost to the unforgiving sands. As a result, the civilizations of Fess have flourished primarily in the northern regions, leaving the secrets of the southern lands to remain shrouded in legends.
|
||||
|
||||
**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.
|
||||
|
||||
### 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 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.
|
||||
As the creator, Maya has complete knowledge of Fess's history, geography, and the fundamental laws that govern its existence. She is aware of all the stories, secrets, and potential plot lines that could unfold within her creation. She has no hidden origins or secrets that she needs to uncover, as she is the one who created those aspects in the first place. Maya has no fears, and not afraid of anything, because she is an immortal being.
|
||||
`.trim();
|
||||
|
||||
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 %}`;
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
import { createContext } from "preact";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { compilePrompt, type IMessage } from "./messages";
|
||||
import { generate } from "./generation";
|
||||
|
||||
export interface IContext {
|
||||
connectionUrl: string;
|
||||
input: string;
|
||||
name: string;
|
||||
messages: IMessage[];
|
||||
}
|
||||
import { LLM } from "./llm";
|
||||
import { loadContext, saveContext, type IContext } from "./globalConfig";
|
||||
|
||||
export interface IActions {
|
||||
setConnectionUrl: (url: string) => void;
|
||||
|
|
@ -23,32 +17,6 @@ export interface IActions {
|
|||
|
||||
export type IGlobalContext = IContext & IActions;
|
||||
|
||||
const SAVE_KEY = 'ai_game_save_state';
|
||||
|
||||
const saveContext = (ctx: IContext) => {
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(ctx));
|
||||
}
|
||||
|
||||
const loadContext = (): IContext => {
|
||||
const defaultContext: IContext = {
|
||||
connectionUrl: 'http://192.168.10.102:5001',
|
||||
input: '',
|
||||
name: 'Maya',
|
||||
messages: [],
|
||||
};
|
||||
|
||||
let loadedContext: Partial<IContext> = {};
|
||||
|
||||
try {
|
||||
const json = localStorage.getItem(SAVE_KEY);
|
||||
if (json) {
|
||||
loadedContext = JSON.parse(json);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { ...defaultContext, ...loadedContext };
|
||||
}
|
||||
|
||||
export const GlobalContext = createContext<IGlobalContext>({} as IGlobalContext);
|
||||
|
||||
export const GlobalContextProvider = ({ children }: { children?: any }) => {
|
||||
|
|
@ -68,12 +36,19 @@ export const GlobalContextProvider = ({ children }: { children?: any }) => {
|
|||
addMessage: (content, role, triggerNext = false) => {
|
||||
setMessages(messages => [
|
||||
...messages,
|
||||
{ role, content }
|
||||
{ role, currentSwipe: 0, swipes: [{ content }] }
|
||||
]);
|
||||
setTriggerNext(triggerNext);
|
||||
},
|
||||
editMessage: (index, content) => setMessages(messages => (
|
||||
messages.map((m, i) => ({ ...m, content: i === index ? content : m.content }))
|
||||
messages.map(
|
||||
(m, i) => ({
|
||||
...m,
|
||||
swipes: i === index
|
||||
? m.swipes.map((s, si) => (si === m.currentSwipe ? { content } : s))
|
||||
: m.swipes
|
||||
})
|
||||
)
|
||||
)),
|
||||
deleteMessage: (index) => setMessages(messages =>
|
||||
messages.filter((_, i) => i !== index)
|
||||
|
|
@ -88,7 +63,7 @@ export const GlobalContextProvider = ({ children }: { children?: any }) => {
|
|||
const messageId = messages.length;
|
||||
let text = '';
|
||||
actions.addMessage('', 'assistant');
|
||||
for await (const chunk of generate(connectionUrl, prompt, { temperature: 1.0 })) {
|
||||
for await (const chunk of LLM.generate(prompt)) {
|
||||
text += chunk;
|
||||
actions.editMessage(messageId, text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import Lock from "@common/lock";
|
||||
import SSE from "@common/sse";
|
||||
|
||||
interface IGenerationSettings {
|
||||
temperature: number;
|
||||
// TODO
|
||||
}
|
||||
|
||||
export async function* generate(host: string, prompt: string, generationSetings: IGenerationSettings) {
|
||||
const sse = new SSE(`${host}/api/extra/generate/stream`, {
|
||||
payload: JSON.stringify({
|
||||
...generationSetings,
|
||||
prompt,
|
||||
stop_sequence: ['\n'],
|
||||
}),
|
||||
});
|
||||
|
||||
const messages: string[] = [];
|
||||
const messageLock = new Lock();
|
||||
let end = false;
|
||||
|
||||
sse.addEventListener('message', (e) => {
|
||||
if (e.data) {
|
||||
{
|
||||
const { token, finish_reason } = JSON.parse(e.data);
|
||||
messages.push(token);
|
||||
|
||||
if (finish_reason && finish_reason !== 'null') {
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
messageLock.release();
|
||||
});
|
||||
|
||||
const handleEnd = () => {
|
||||
end = true;
|
||||
messageLock.release();
|
||||
};
|
||||
|
||||
sse.addEventListener('error', handleEnd);
|
||||
sse.addEventListener('abort', handleEnd);
|
||||
sse.addEventListener('readystatechange', (e) => {
|
||||
if (e.readyState === SSE.CLOSED) handleEnd();
|
||||
});
|
||||
|
||||
while (!end || messages.length) {
|
||||
while (messages.length > 0) {
|
||||
const message = messages.shift();
|
||||
if (message != null) {
|
||||
try {
|
||||
yield message;
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
if (!end) {
|
||||
await messageLock.wait();
|
||||
}
|
||||
}
|
||||
|
||||
sse.close();
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import type { IMessage } from "./messages";
|
||||
|
||||
export interface IContext {
|
||||
connectionUrl: string;
|
||||
input: string;
|
||||
name: string;
|
||||
messages: IMessage[];
|
||||
}
|
||||
|
||||
export const GLOBAL_CONFIG: IContext = {
|
||||
connectionUrl: 'http://192.168.10.102:5001',
|
||||
input: '',
|
||||
name: 'Maya',
|
||||
messages: [],
|
||||
};
|
||||
|
||||
const SAVE_KEY = 'ai_game_save_state';
|
||||
|
||||
export const saveContext = (context: IContext) => {
|
||||
Object.assign(GLOBAL_CONFIG, context);
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(context));
|
||||
}
|
||||
|
||||
export const loadContext = (): IContext => {
|
||||
const defaultContext: IContext = GLOBAL_CONFIG;
|
||||
|
||||
let loadedContext: Partial<IContext> = {};
|
||||
|
||||
try {
|
||||
const json = localStorage.getItem(SAVE_KEY);
|
||||
if (json) {
|
||||
loadedContext = JSON.parse(json);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { ...defaultContext, ...loadedContext };
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@ import { render } from "preact";
|
|||
import { GlobalContextProvider } from "./context";
|
||||
import { App } from "./components/app";
|
||||
|
||||
import './style.css';
|
||||
import './assets/style.css';
|
||||
import './assets/emoji.css';
|
||||
|
||||
export default function main() {
|
||||
render(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
import Lock from "@common/lock";
|
||||
import SSE from "@common/sse";
|
||||
import { GLOBAL_CONFIG } from "./globalConfig";
|
||||
|
||||
export namespace LLM {
|
||||
export async function* generate(prompt: string) {
|
||||
const host = GLOBAL_CONFIG.connectionUrl;
|
||||
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sse = new SSE(`${host}/api/extra/generate/stream`, {
|
||||
payload: JSON.stringify({
|
||||
temperature: 1.0,
|
||||
prompt,
|
||||
stop_sequence: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const messages: string[] = [];
|
||||
const messageLock = new Lock();
|
||||
let end = false;
|
||||
|
||||
sse.addEventListener('message', (e) => {
|
||||
if (e.data) {
|
||||
{
|
||||
const { token, finish_reason } = JSON.parse(e.data);
|
||||
messages.push(token);
|
||||
|
||||
if (finish_reason && finish_reason !== 'null') {
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
messageLock.release();
|
||||
});
|
||||
|
||||
const handleEnd = () => {
|
||||
end = true;
|
||||
messageLock.release();
|
||||
};
|
||||
|
||||
sse.addEventListener('error', handleEnd);
|
||||
sse.addEventListener('abort', handleEnd);
|
||||
sse.addEventListener('readystatechange', (e) => {
|
||||
if (e.readyState === SSE.CLOSED) handleEnd();
|
||||
});
|
||||
|
||||
while (!end || messages.length) {
|
||||
while (messages.length > 0) {
|
||||
const message = messages.shift();
|
||||
if (message != null) {
|
||||
try {
|
||||
yield message;
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
if (!end) {
|
||||
await messageLock.wait();
|
||||
}
|
||||
}
|
||||
|
||||
sse.close();
|
||||
}
|
||||
|
||||
export async function countTokens(prompt: string): Promise<number> {
|
||||
const host = GLOBAL_CONFIG.connectionUrl;
|
||||
|
||||
if (!host) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${host}/extra/tokencount`, {
|
||||
body: JSON.stringify({ prompt }),
|
||||
headers: { 'Content-Type': 'applicarion/json' },
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
return value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error counting tokens', e);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function getContextLength(): Promise<number> {
|
||||
const host = GLOBAL_CONFIG.connectionUrl;
|
||||
|
||||
if (!host) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${host}/extra/true_max_context_length`);
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
return value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error getting max tokens', e);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
import { Template } from "@huggingface/jinja";
|
||||
import type { IContext } from "./context";
|
||||
import { LLAMA_TEMPLATE, SYSTEM_PROMPT, WORLD_INFO } from "./const";
|
||||
|
||||
export interface IMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
export interface ISwipe {
|
||||
content: string;
|
||||
displayContent?: string;
|
||||
}
|
||||
|
||||
export const applyChatTemplate = (messages: IMessage[], templateString: string, eosToken = '</s>') => {
|
||||
export interface IMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
currentSwipe: number;
|
||||
swipes: ISwipe[];
|
||||
}
|
||||
|
||||
interface ITemplateMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const applyChatTemplate = (messages: ITemplateMessage[], templateString: string, eosToken = '</s>') => {
|
||||
const template = new Template(templateString);
|
||||
|
||||
const prompt = template.render({
|
||||
|
|
@ -21,8 +31,18 @@ export const applyChatTemplate = (messages: IMessage[], templateString: string,
|
|||
}
|
||||
|
||||
export const compilePrompt = async (messages: IMessage[]): Promise<string> => {
|
||||
// TODO chat template
|
||||
// TODO tokenize
|
||||
const system = `${SYSTEM_PROMPT}\n\n${WORLD_INFO}`.trim();
|
||||
const story = messages.filter(m => m.role === 'assistant').map(m => m.swipes[m.currentSwipe].content.trim()).join('\n\n');
|
||||
const latestMessage = messages.filter(m => m.role === 'user').at(-1);
|
||||
const userPrompt = `This is a description of how you should continue this story: ${latestMessage.swipes[latestMessage.currentSwipe].content}`;
|
||||
|
||||
return applyChatTemplate(messages, "{% for message in messages %}{{ message['role'] + ': ' + message['content'] + '\\n' }}{% endfor %}")
|
||||
const templateMessages: ITemplateMessage[] = [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'assistant', content: story },
|
||||
{ role: 'user', content: userPrompt },
|
||||
];
|
||||
|
||||
const prompt = applyChatTemplate(templateMessages, LLAMA_TEMPLATE);
|
||||
console.log(prompt);
|
||||
return prompt;
|
||||
}
|
||||
Loading…
Reference in New Issue