Skip to content

Commit 46f169f

Browse files
committed
feat: image config type and data skeleton handling
1 parent 2c3b1e9 commit 46f169f

File tree

6 files changed

+124
-8
lines changed

6 files changed

+124
-8
lines changed

frontend/src/js/core/schema.ts

+8
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ export function buildSchema(data: any): SchemaRoot {
205205
case 'bigint':
206206
break;
207207
case 'string':
208+
if (val.startsWith('!IMAGE')) {
209+
return {
210+
type: 'string',
211+
inputType: 'Image',
212+
key: path[path.length - 1],
213+
default: val,
214+
};
215+
}
208216
return {
209217
type: 'string',
210218
inputType: 'Text',

frontend/src/js/core/templating.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ export type GlobalState = {
180180
aiToken?: string;
181181
};
182182

183+
const dataURIsCommitted: Record<string, boolean> = {};
184+
183185
/**
184186
* Render template with given state.
185187
* @param template Template string.
@@ -212,9 +214,51 @@ export const render = (
212214
clonedState.aiToken = ai.value.token;
213215
}
214216

217+
const cleanupImages = (obj: any, parent: any, key: string) => {
218+
if (obj === null || obj === undefined) {
219+
return;
220+
}
221+
222+
switch (typeof obj) {
223+
case 'string':
224+
if (obj.startsWith('!IMAGE:')) {
225+
const stripped = obj.replace('!IMAGE:', '');
226+
if (stripped.startsWith('http') || stripped.startsWith('data:')) {
227+
parent[key] = stripped;
228+
} else {
229+
parent[key] = 'https://dummyimage.com/' + stripped;
230+
}
231+
} else if (obj.startsWith('!IMAGE')) {
232+
parent[key] = 'https://dummyimage.com/400x3:2';
233+
}
234+
235+
if (parent[key].startsWith('data:')) {
236+
const imageHash = new jsSHA('SHA-256', 'TEXT', { encoding: 'UTF8' }).update(parent[key]).getHash('HEX');
237+
238+
if (!dataURIsCommitted[imageHash]) {
239+
const request = new XMLHttpRequest();
240+
request.open("POST", "/api/image-cache", false); // `false` makes the request synchronous
241+
request.send(JSON.stringify(parent[key]));
242+
}
243+
244+
parent[key] = `/api/image-cache/${imageHash}`;
245+
}
246+
return;
247+
case 'object':
248+
Object.keys(obj).forEach((key) => {
249+
cleanupImages(obj[key], obj, key);
250+
});
251+
break;
252+
default:
253+
return;
254+
}
255+
};
256+
257+
cleanupImages((clonedState as any).it, null, '');
258+
cleanupImages((clonedState as any).config, null, '');
259+
215260
Object.keys(clonedState.images).forEach((key) => {
216261
const imageHash = new jsSHA('SHA-256', 'TEXT', { encoding: 'UTF8' }).update(clonedState.images[key]).getHash('HEX');
217-
console.log(imageHash);
218262
clonedState.images[key] = `/api/image-cache/${imageHash}`;
219263
});
220264

frontend/src/js/ui/components/config/creator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default (): m.Component<ConfigCreatorProps> => ({
8484
]), //
8585
m(DividerVert, { noSpacing: true }),
8686
m(
87-
'div.pa2.bg--black-05',
87+
'div.pa2.bg--black-05.w-100',
8888
m(types[c.type].view, {
8989
value: c.default,
9090
inEdit: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import m from 'mithril';
2+
3+
import { css } from 'goober';
4+
5+
import Icon from 'js/ui/components/atomic/icon';
6+
import MiniHeader from 'js/ui/components/atomic/mini-header';
7+
import Config, { ConfigProps } from 'js/ui/components/config/config';
8+
import ImageUpload from 'js/ui/components/image-upload';
9+
import Flex from 'js/ui/components/layout/flex';
10+
11+
const imageClass = css`
12+
max-width: 80px;
13+
`;
14+
15+
export default {
16+
name: 'Image',
17+
default: () => '!IMAGE',
18+
view: (): m.Component<ConfigProps> => ({
19+
view: ({ attrs }) => [
20+
!attrs.inEdit ? null : m(MiniHeader, 'Default'),
21+
attrs.value && attrs.value.length > 0 && attrs.value.startsWith('data:')
22+
? m(Flex, { items: 'center', justify: 'end' }, [
23+
m(`img.br2.mr2.w-100.${imageClass}`, { src: attrs.value }), //
24+
m(
25+
Icon,
26+
{
27+
icon: 'trash',
28+
size: 4,
29+
className: '.col-error',
30+
onClick: () => {
31+
if (!attrs.onChange) return;
32+
attrs.onChange("");
33+
},
34+
},
35+
'Remove',
36+
)
37+
])
38+
: m(ImageUpload, {
39+
height: 50,
40+
compact: true,
41+
className: '.mb3',
42+
onUpload: (name, image) => {
43+
if (!attrs.onChange) return;
44+
45+
m.request({
46+
url: `/api/image-cache`,
47+
method: 'POST',
48+
body: image,
49+
}).then(() => {
50+
if (!attrs.onChange) return;
51+
attrs.onChange(image);
52+
});
53+
},
54+
}),
55+
],
56+
}),
57+
} as Config;

frontend/src/js/ui/components/config/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Checkbox from './checkbox';
22
import DataSources from './data-source';
33
import FilePath from './file-path';
44
import FolderPath from './folder-path';
5+
import Image from './image';
56
import MultipleOptions from './multiple-options';
67
import Number from './number';
78
import Options from './options';
@@ -18,4 +19,5 @@ export default {
1819
DataSources,
1920
FilePath,
2021
FolderPath,
22+
Image,
2123
};

frontend/src/js/ui/components/image-upload.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Icon from 'js/ui/components/atomic/icon';
1010
import Flex from 'js/ui/components/layout/flex';
1111

1212
type ImageUploadProps = {
13+
compact?: boolean;
1314
className?: string;
1415
height?: number;
1516
onUpload?: (name: string, base: string) => void;
@@ -30,8 +31,12 @@ export default (): m.Component<ImageUploadProps> => {
3031
'label.pointer.db.h-100.w-100.ba.bw1.br2.b--dashed.b--col-primary-muted.bg-black-05',
3132
{ style: { height: attrs.height ? attrs.height + 'px' : '250px' }, for: id },
3233
[
33-
m(Flex, { className: '.w-100.h-100', justify: 'center', items: 'center', direction: 'column' }, [
34-
m(Icon, { icon: 'document', size: 2, className: '.mb3' }),
34+
m(Flex, { className: '.w-100.h-100', justify: 'center', items: 'center', direction: attrs.compact ? 'row' : 'column' }, [
35+
m(Icon, {
36+
icon: 'document',
37+
size: attrs.compact ? 4 : 2,
38+
className: attrs.compact ? '.mr3' : '.mb3',
39+
}),
3540
m('div.fw5', 'Select image from your computer'),
3641
]), //
3742
],
@@ -59,17 +64,17 @@ export default (): m.Component<ImageUploadProps> => {
5964
}),
6065
//
6166
// Divider
62-
m(Icon, { icon: 'more', size: 3, className: '.mv3.o-50' }),
67+
attrs.compact ? m('div.mb2') : m(Icon, { icon: 'more', size: 3, className: '.mv3.o-50' }),
6368
//
6469
// Upload from URL
65-
m('div.db.h-100.w-100.ba.bw1.br2.b--dashed.b--col-primary-muted.bg-black-05.pa3', [
70+
m('div.db.h-100.w-100.ba.bw1.br2.b--dashed.b--col-primary-muted.bg-black-05' + (attrs.compact ? '.pa2' : '.pa3'), [
6671
m('div.tc.mb3.fw5', 'Download from URL'),
67-
m(Flex, [
72+
m(Flex, { direction: attrs.compact ? 'column' : 'row' }, [
6873
m(Input, { className: '.w-100', placeholder: 'https://example.com/image.png', value: url }), //
6974
m(
7075
Button,
7176
{
72-
className: '.ml2',
77+
className: attrs.compact ? '.mt2' : '.ml2',
7378
intend: 'primary',
7479
onClick: () => {
7580
if (url.length === 0) return;

0 commit comments

Comments
 (0)