includio-cms 0.0.35 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/components/media/alt-input.svelte +46 -7
- package/dist/admin/components/media/media-library.svelte +52 -50
- package/dist/admin/remote/ai.remote.d.ts +3 -0
- package/dist/admin/remote/ai.remote.js +8 -0
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/ai-openai/index.d.ts +2 -0
- package/dist/ai-openai/index.js +61 -0
- package/dist/core/cms.d.ts +2 -0
- package/dist/core/cms.js +2 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +9 -1
- package/dist/files-local/index.js +5 -4
- package/dist/types/adapters/ai.d.ts +11 -0
- package/dist/types/adapters/ai.js +1 -0
- package/dist/types/adapters/files.d.ts +2 -2
- package/dist/types/cms.d.ts +3 -0
- package/package.json +8 -3
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import Input from '../../../components/ui/input/input.svelte';
|
|
4
4
|
|
|
5
5
|
import { getRemotes } from '../../context/remotes.js';
|
|
6
|
+
import Sparkles from '@tabler/icons-svelte/icons/sparkles';
|
|
7
|
+
import { toast } from 'svelte-sonner';
|
|
6
8
|
|
|
7
9
|
const remotes = getRemotes();
|
|
8
10
|
|
|
@@ -14,18 +16,55 @@
|
|
|
14
16
|
let { alt, fileId }: Props = $props();
|
|
15
17
|
|
|
16
18
|
let newAlt = $state(alt ?? '');
|
|
19
|
+
|
|
20
|
+
let input: HTMLInputElement | null = $state(null);
|
|
21
|
+
|
|
22
|
+
let generating = $state(false);
|
|
23
|
+
|
|
24
|
+
async function generateAltText() {
|
|
25
|
+
if (!input) return;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
input.disabled = true;
|
|
29
|
+
generating = true;
|
|
30
|
+
|
|
31
|
+
newAlt = await remotes.generateAltText({ fileId });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error generating alt text:', error);
|
|
34
|
+
toast.error('Failed to generate alt text');
|
|
35
|
+
} finally {
|
|
36
|
+
input.disabled = false;
|
|
37
|
+
generating = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
17
40
|
</script>
|
|
18
41
|
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
<div class="flex items-center gap-2">
|
|
43
|
+
<Input
|
|
44
|
+
bind:ref={input}
|
|
45
|
+
bind:value={newAlt}
|
|
46
|
+
placeholder="Wprowadź tekst alternatywny"
|
|
47
|
+
class="w-full"
|
|
48
|
+
aria-label="Tekst alternatywny obrazu"
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
size="icon"
|
|
52
|
+
onclick={generateAltText}
|
|
53
|
+
disabled={generating}
|
|
54
|
+
aria-label="Generate alt text"
|
|
55
|
+
>
|
|
56
|
+
<Sparkles class={generating ? 'animate-bounce' : ''} />
|
|
57
|
+
</Button>
|
|
58
|
+
</div>
|
|
25
59
|
|
|
26
60
|
<Button
|
|
27
61
|
onclick={() => {
|
|
28
|
-
|
|
62
|
+
try {
|
|
63
|
+
remotes.setMediaFileAlt({ fileId, alt: newAlt });
|
|
64
|
+
toast.success('Alt text saved');
|
|
65
|
+
} catch {
|
|
66
|
+
toast.error('Failed to save alt text');
|
|
67
|
+
}
|
|
29
68
|
}}
|
|
30
69
|
class="mt-2"
|
|
31
70
|
disabled={!newAlt.trim() || (alt !== null && newAlt === alt)}>Save alt</Button
|
|
@@ -132,63 +132,65 @@
|
|
|
132
132
|
|
|
133
133
|
<Item.Root class="w-[25.375rem] shrink-0 p-0" variant="outline">
|
|
134
134
|
<Item.Content class="h-full">
|
|
135
|
-
{#
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<div class="
|
|
147
|
-
<div class="
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</div>
|
|
154
|
-
<div class="mr-2.5 w-[50.2%] shrink-0 space-y-4">
|
|
155
|
-
<div class="grid grid-cols-1 gap-2">
|
|
156
|
-
<Label>Type</Label>
|
|
157
|
-
<Input
|
|
158
|
-
disabled
|
|
159
|
-
value={folders.find((folder) => folder.id === currentFile?.folderId)?.name}
|
|
135
|
+
{#key currentFile}
|
|
136
|
+
{#if currentFile}
|
|
137
|
+
<div class="flex items-center justify-end border-b px-6 py-3">
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
size="sm"
|
|
141
|
+
class="mr-2.5"
|
|
142
|
+
variant="destructive"
|
|
143
|
+
onclick={deleteFileCommand}>Delete</Button
|
|
144
|
+
>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="space-y-6 p-6">
|
|
147
|
+
<div class="mb-6 flex justify-between gap-6">
|
|
148
|
+
<div class="aspect-square w-[40.2%] shrink-0 overflow-hidden rounded-2xl border p-1">
|
|
149
|
+
<img
|
|
150
|
+
src={currentFile.url}
|
|
151
|
+
alt={currentFile.name}
|
|
152
|
+
class="size-full rounded-lg object-contain"
|
|
160
153
|
/>
|
|
161
154
|
</div>
|
|
162
|
-
<div class="
|
|
163
|
-
<
|
|
164
|
-
|
|
155
|
+
<div class="mr-2.5 w-[50.2%] shrink-0 space-y-4">
|
|
156
|
+
<div class="grid grid-cols-1 gap-2">
|
|
157
|
+
<Label>Type</Label>
|
|
158
|
+
<Input
|
|
159
|
+
disabled
|
|
160
|
+
value={folders.find((folder) => folder.id === currentFile?.folderId)?.name}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="grid grid-cols-1 gap-2">
|
|
164
|
+
<Label>Name</Label>
|
|
165
|
+
<Input disabled value={currentFile.name} />
|
|
166
|
+
</div>
|
|
165
167
|
</div>
|
|
166
168
|
</div>
|
|
167
|
-
</div>
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
<div class="grid grid-cols-1 gap-2">
|
|
171
|
+
<Label>URL</Label>
|
|
172
|
+
<Input disabled value={currentFile.url} />
|
|
173
|
+
</div>
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
<div class="grid grid-cols-1 gap-2">
|
|
176
|
+
<Label>Alt text</Label>
|
|
177
|
+
<AltInput alt={currentFile.alt} fileId={currentFile.id} />
|
|
178
|
+
</div>
|
|
177
179
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
</
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{/
|
|
180
|
+
<div class="mt-auto p-6">
|
|
181
|
+
<p class="text-muted-foregound text-xs">
|
|
182
|
+
Created at: <span class="text-foreground"
|
|
183
|
+
>{new Date(currentFile.createdAt).toLocaleString()}</span
|
|
184
|
+
>
|
|
185
|
+
</p>
|
|
186
|
+
<p class="text-muted-foregound text-xs">
|
|
187
|
+
ID: <span class="text-foreground">{currentFile.id}</span>
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
{:else}
|
|
191
|
+
Select a file to see details
|
|
192
|
+
{/if}
|
|
193
|
+
{/key}
|
|
192
194
|
</Item.Content>
|
|
193
195
|
</Item.Root>
|
|
194
196
|
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { command } from '$app/server';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
export const generateAltText = command(z.object({
|
|
5
|
+
fileId: z.string().uuid()
|
|
6
|
+
}), async ({ fileId }) => {
|
|
7
|
+
return getCMS().aiAdapter?.generateAltText(fileId) || '';
|
|
8
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getCMS } from '../core/cms.js';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
import { zodResponseFormat } from 'openai/helpers/zod.mjs';
|
|
4
|
+
import z from 'zod';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
export function openAIAdapter(config) {
|
|
7
|
+
const openai = new OpenAI({
|
|
8
|
+
apiKey: config.apiKey
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
generateAltText: async (fileId) => {
|
|
12
|
+
const altTextSchema = z.object({
|
|
13
|
+
altText: z.string().min(1)
|
|
14
|
+
});
|
|
15
|
+
const mediaFile = await getCMS().databaseAdapter.getMediaFile({
|
|
16
|
+
data: {
|
|
17
|
+
id: fileId
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (!mediaFile) {
|
|
21
|
+
throw new Error('Media file not found in database');
|
|
22
|
+
}
|
|
23
|
+
const file = await getCMS().filesAdapter.downloadFile(mediaFile.url.split('/').pop() || '');
|
|
24
|
+
if (!file) {
|
|
25
|
+
throw new Error('File not found');
|
|
26
|
+
}
|
|
27
|
+
// Konwertuj plik na PNG używając Sharp i przekonwertuj na base64
|
|
28
|
+
const fileBuffer = Buffer.from(await file.arrayBuffer());
|
|
29
|
+
const pngBuffer = await sharp(fileBuffer).png().toBuffer();
|
|
30
|
+
const imageBase64 = pngBuffer.toString('base64');
|
|
31
|
+
const prompt = `Generate a concise and descriptive alt text for the following image file in polish language. The alt text should accurately describe the content and context of the image, be no longer than 125 characters, and avoid using phrases like "image of" or "picture of".`;
|
|
32
|
+
const completion = await openai.chat.completions.parse({
|
|
33
|
+
// model: 'gpt-4.1-nano',
|
|
34
|
+
model: 'gpt-4o',
|
|
35
|
+
messages: [
|
|
36
|
+
{
|
|
37
|
+
role: 'user',
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: prompt
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'image_url',
|
|
45
|
+
image_url: {
|
|
46
|
+
url: `data:image/png;base64,${imageBase64}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
response_format: zodResponseFormat(altTextSchema, 'response')
|
|
53
|
+
});
|
|
54
|
+
const response = completion.choices[0].message;
|
|
55
|
+
if (!response.parsed) {
|
|
56
|
+
throw new Error('AI response did not match expected format');
|
|
57
|
+
}
|
|
58
|
+
return response.parsed.altText;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/core/cms.d.ts
CHANGED
|
@@ -6,10 +6,12 @@ import type { Language } from '../types/languages.js';
|
|
|
6
6
|
import type { SingleConfigWithType } from '../types/singles.js';
|
|
7
7
|
import type { PluginConfig } from '../types/plugins.js';
|
|
8
8
|
import type { FormConfig } from '../types/forms.js';
|
|
9
|
+
import type { AIAdapter } from '../types/adapters/ai.js';
|
|
9
10
|
export declare class CMS implements ICMS {
|
|
10
11
|
private config;
|
|
11
12
|
databaseAdapter: DatabaseAdapter;
|
|
12
13
|
filesAdapter: FilesAdapter;
|
|
14
|
+
aiAdapter: AIAdapter | null;
|
|
13
15
|
collections: Record<string, CollectionConfigWithType>;
|
|
14
16
|
singles: Record<string, SingleConfigWithType>;
|
|
15
17
|
forms: Record<string, FormConfig>;
|
package/dist/core/cms.js
CHANGED
|
@@ -3,6 +3,7 @@ export class CMS {
|
|
|
3
3
|
config;
|
|
4
4
|
databaseAdapter;
|
|
5
5
|
filesAdapter;
|
|
6
|
+
aiAdapter = null;
|
|
6
7
|
collections;
|
|
7
8
|
singles;
|
|
8
9
|
forms;
|
|
@@ -12,6 +13,7 @@ export class CMS {
|
|
|
12
13
|
this.config = config;
|
|
13
14
|
this.databaseAdapter = config.db;
|
|
14
15
|
this.filesAdapter = config.files;
|
|
16
|
+
this.aiAdapter = config.ai || null;
|
|
15
17
|
// Initialize auth service
|
|
16
18
|
initAuth(config.auth.adapter(config.auth.config, this.databaseAdapter), {
|
|
17
19
|
cookieName: config.auth.config.sessionCookieName
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { getCMS } from '../../../../cms.js';
|
|
2
2
|
import sharp from 'sharp';
|
|
3
3
|
export async function generateImageStyle(mediaFileId, style) {
|
|
4
|
-
const
|
|
4
|
+
const mediaFile = await getCMS().databaseAdapter.getMediaFile({
|
|
5
|
+
data: {
|
|
6
|
+
id: mediaFileId
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
if (!mediaFile) {
|
|
10
|
+
throw new Error('Media file not found in database');
|
|
11
|
+
}
|
|
12
|
+
const file = await getCMS().filesAdapter.downloadFile(mediaFile.url.split('/').pop() || '');
|
|
5
13
|
if (!file) {
|
|
6
14
|
throw new Error('Media file not found');
|
|
7
15
|
}
|
|
@@ -53,14 +53,15 @@ async function processVideo(filepath, filename) {
|
|
|
53
53
|
}
|
|
54
54
|
export function local() {
|
|
55
55
|
return {
|
|
56
|
-
|
|
57
|
-
const filepath = resolve(fullDir,
|
|
56
|
+
downloadFile: async (filename) => {
|
|
57
|
+
const filepath = resolve(fullDir, filename);
|
|
58
58
|
try {
|
|
59
59
|
const data = await readFile(filepath);
|
|
60
|
-
const file = new File([data],
|
|
60
|
+
const file = new File([data], filename);
|
|
61
61
|
return file;
|
|
62
62
|
}
|
|
63
|
-
catch {
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.log(e);
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
66
67
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface AIConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
}
|
|
4
|
+
export type AIAdapterConfig = {
|
|
5
|
+
config: AIConfig;
|
|
6
|
+
adapter: (config: AIConfig, db: AIAdapter) => AIAdapter;
|
|
7
|
+
};
|
|
8
|
+
export interface AIAdapter {
|
|
9
|
+
generateAltText: GenerateAltText;
|
|
10
|
+
}
|
|
11
|
+
export type GenerateAltText = (fileId: string) => Promise<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UploadedMediaFile } from '../media.js';
|
|
2
2
|
export interface FilesAdapter {
|
|
3
|
-
|
|
3
|
+
downloadFile: DownloadFile;
|
|
4
4
|
uploadFile: UploadFile;
|
|
5
5
|
}
|
|
6
|
-
export type
|
|
6
|
+
export type DownloadFile = (id: string) => Promise<File | null>;
|
|
7
7
|
export type UploadFile = (file: File) => Promise<UploadedMediaFile>;
|
package/dist/types/cms.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Language } from './languages.js';
|
|
|
6
6
|
import type { SingleConfig, SingleConfigWithType } from './singles.js';
|
|
7
7
|
import type { PluginConfig } from './plugins.js';
|
|
8
8
|
import type { FormConfig } from './forms.js';
|
|
9
|
+
import type { AIAdapter } from './adapters/ai.js';
|
|
9
10
|
export interface CMSConfig {
|
|
10
11
|
languages: Language[];
|
|
11
12
|
collections?: CollectionConfig[];
|
|
@@ -15,6 +16,7 @@ export interface CMSConfig {
|
|
|
15
16
|
files: FilesAdapter;
|
|
16
17
|
auth: AuthAdapterConfig;
|
|
17
18
|
plugins?: PluginConfig[];
|
|
19
|
+
ai?: AIAdapter;
|
|
18
20
|
}
|
|
19
21
|
export interface ICMS {
|
|
20
22
|
collections: Record<string, CollectionConfigWithType>;
|
|
@@ -24,4 +26,5 @@ export interface ICMS {
|
|
|
24
26
|
databaseAdapter: DatabaseAdapter;
|
|
25
27
|
filesAdapter: FilesAdapter;
|
|
26
28
|
plugins: PluginConfig[];
|
|
29
|
+
aiAdapter: AIAdapter | null;
|
|
27
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "includio-cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.36",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -92,6 +92,10 @@
|
|
|
92
92
|
"./auth-lucia": {
|
|
93
93
|
"types": "./dist/auth-lucia/index.d.ts",
|
|
94
94
|
"node": "./dist/auth-lucia/index.js"
|
|
95
|
+
},
|
|
96
|
+
"./ai-openai": {
|
|
97
|
+
"types": "./dist/ai-openai/index.d.ts",
|
|
98
|
+
"node": "./dist/ai-openai/index.js"
|
|
95
99
|
}
|
|
96
100
|
},
|
|
97
101
|
"peerDependencies": {
|
|
@@ -124,6 +128,7 @@
|
|
|
124
128
|
"@types/node": "^22",
|
|
125
129
|
"clsx": "^2.1.1",
|
|
126
130
|
"drizzle-kit": "^0.30.2",
|
|
131
|
+
"drizzle-orm": "^0.40.0",
|
|
127
132
|
"eslint": "^9.18.0",
|
|
128
133
|
"eslint-config-prettier": "^10.0.1",
|
|
129
134
|
"eslint-plugin-storybook": "9.0.6",
|
|
@@ -144,8 +149,7 @@
|
|
|
144
149
|
"typescript": "^5.0.0",
|
|
145
150
|
"typescript-eslint": "^8.20.0",
|
|
146
151
|
"vite": "^7.0.4",
|
|
147
|
-
"vitest": "^3.2.3"
|
|
148
|
-
"drizzle-orm": "^0.40.0"
|
|
152
|
+
"vitest": "^3.2.3"
|
|
149
153
|
},
|
|
150
154
|
"keywords": [
|
|
151
155
|
"svelte"
|
|
@@ -168,6 +172,7 @@
|
|
|
168
172
|
"fast-glob": "^3.3.3",
|
|
169
173
|
"fluent-ffmpeg": "^2.1.3",
|
|
170
174
|
"mode-watcher": "^1.0.8",
|
|
175
|
+
"openai": "^6.7.0",
|
|
171
176
|
"path-to-regexp": "^8.2.0",
|
|
172
177
|
"postgres": "^3.4.5",
|
|
173
178
|
"readline": "^1.3.0",
|