${p.name}`,
21 | `${p.type}`,
22 | cleanValue(p.value),
23 | p.description ? p?.description : '-'
24 | ];
25 | })
26 | };
27 | }
28 |
29 | // Mapper: Slots
30 | export function sveldMapperSlots(component: Component): TableSource {
31 | const { slots } = component.sveld;
32 | const slotsHeadings = ['Name', 'Default', 'Fallback', 'Description'];
33 | return {
34 | head: slotsHeadings,
35 | body: slots.map((s: any) => {
36 | // prettier-ignore
37 | return [
38 | `${s.name.replaceAll('__', '')}
`,
39 | s.default ? '✓' : '-',
40 | s.fallback ? '✓' : '-',
41 | // s.slot_props ? s.slot_props : '-', // NOTE: we don't currently use this
42 | s.description ? s.description : '-'
43 | ];
44 | })
45 | };
46 | }
47 |
48 | // Mapper: Events
49 | export function sveldeMapperEvents(component: Component): TableSource {
50 | const { events } = component.sveld;
51 | const eventsHeadings = ['Name', 'Type', 'Element', 'Response', 'Description'];
52 | return {
53 | head: eventsHeadings,
54 | body: events.map((e: any) => {
55 | // prettier-ignore
56 | return [
57 | `on:${e.name}
`,
58 | `${e.type}`,
59 | e.element ? escapeHtml(`<${e.element}>`) : '-',
60 | e.detail ? e.detail : '-',
61 | e.description ? e.description : '-'
62 | ];
63 | })
64 | };
65 | }
66 |
67 | // ---
68 |
69 | // prettier-ignore
70 | /**
71 | * @param unsafe The unsafe raw HTML string
72 | * @returns a nice, safely escaped string
73 | * @see https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
74 | */
75 | function escapeHtml(unsafe: string) {
76 | return unsafe
77 | .replace(/&/g, "&")
78 | .replace(//g, ">")
80 | .replace(/"/g, """)
81 | .replace(/'/g, "'");
82 | }
83 |
84 | // ---
85 |
86 | /*
87 | FROM CHRIS:
88 | Let's aim for something more like this. Rather than trying to format every
89 | description element in every single Sveld item, we handle it per instance.
90 | The idea being that prop desc tags may differ from slot desc tags. It also
91 | allows us to pass unique settings per instance rather than just assuming
92 | they'll all be the same.
93 |
94 | Likewise we might follow the lead above and have unique versions of the desc parser
95 | per each type of data as well so they can be purpose-built for only the description
96 | data they are handling. Though the universal method above may suffice.
97 | */
98 | // function propDescTagParser(d) {}
99 |
100 | // ----
101 |
102 | // export function parseDescriptionAndTagsFromJSDocs(jsDocs: string) {
103 | // let description: string = '';
104 | // let tags: { [key: string]: string } = {};
105 | // let capturingDescription = true;
106 | // let currentTag = '';
107 | // let currentCapture = '';
108 | // let currentChar = '';
109 |
110 | // for (let x = 0; x < jsDocs.length; x++) {
111 | // currentChar = jsDocs[x];
112 | // switch (currentChar) {
113 | // case '@':
114 | // //tag at front wipes out description
115 | // if (capturingDescription && x < 1) {
116 | // capturingDescription = false;
117 | // }
118 | // // normal capture of leading description till a tag
119 | // if (capturingDescription && currentCapture.length > 1) {
120 | // description = currentCapture;
121 | // currentCapture = '';
122 | // capturingDescription = false;
123 | // }
124 | // //two tags in a row, stash the first before capturing name of second below.
125 | // if (currentTag.length > 1) {
126 | // tags[currentTag] = currentCapture;
127 | // currentTag = '';
128 | // currentCapture = '';
129 | // }
130 | // //at start of tag, seek till space char for tag name
131 | // while (x != jsDocs.length && currentChar != ' ') {
132 | // currentTag += currentChar;
133 | // x++;
134 | // currentChar = jsDocs[x];
135 | // }
136 | // break;
137 | // default:
138 | // // capturing value of description or tag
139 | // currentCapture += currentChar;
140 | // break;
141 | // }
142 | // }
143 | // //if a tag finishes the jsDoc string
144 | // if (currentTag.length) {
145 | // tags[currentTag] = currentCapture;
146 | // }
147 | // return [description, tags];
148 | // }
149 |
--------------------------------------------------------------------------------
/src/lib_skeleton/Shell/types.ts:
--------------------------------------------------------------------------------
1 | export interface Component {
2 | /** Provide a semantic component label. */
3 | label?: string;
4 | /** Provide HTML markup for a props description. */
5 | descProps?: string;
6 | /** Provide HTML markup for a slots description. */
7 | descSlots?: string;
8 | /** Provide HTML markup for a events description. */
9 | descEvents?: string;
10 | /** Provide a list of props that children can override. */
11 | overrideProps?: string[];
12 | /** Provide the raw component Sveld doc source. */
13 | sveld: any; // SveldJson; // FIXME: we need to resolve this type
14 | }
15 |
16 | export interface SveldJson {
17 | name?: string;
18 | type?: string;
19 | description?: string;
20 | value?: string;
21 | detail?: string;
22 | element?: string;
23 | tags?: {
24 | tag: string;
25 | value?: string;
26 | }[];
27 | [key: string]: unknown;
28 | }
29 |
30 | export interface ShellSettings {
31 | /** The feature name. */
32 | name: string;
33 | /** The feature description */
34 | description?: string;
35 | /** Show Table of contents */
36 | toc?: boolean;
37 | /** Component documentation, which utilizes Sveld. */
38 | components?: Component[];
39 | /** Component element that uses restProps */
40 | restProps?: string;
41 | /** Action parameter table source [prop, type, default, values, description] */
42 | parameters?: [string, string, string, string, string][];
43 | /** Tailwind Element classes table source [name, values, description] */
44 | classes?: [string, string, string][];
45 | /** Keyboard interaction table source [name, description]. */
46 | keyboard?: [string, string][];
47 | }
48 |
--------------------------------------------------------------------------------
/src/routes/(inner)/+layout.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from '@sveltejs/kit';
2 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
3 |
4 | // types
5 | import type { LayoutLoad } from './$types';
6 |
7 | export const load: LayoutLoad = async (event) => {
8 | const { session } = await getSupabase(event);
9 | if (!session) {
10 | throw redirect(307, '/');
11 | }
12 | return event;
13 | };
14 |
--------------------------------------------------------------------------------
/src/routes/(inner)/chat/+page.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
53 |
54 | Model
58 | 4.1-nano
61 | 4.1
64 |
65 | {#if $activeModel === 'gpt-4.1'}
66 | Due to high costs, GPT-4.1 currently works only with your own OpenAI API Key. Set your
68 | key in your profile settings
69 | .
71 | {/if}
72 |
73 | Agent
74 |
75 | -
76 |
83 |
84 | -
85 |
92 |
93 | -
94 |
101 |
102 |
103 |
104 |
105 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/src/routes/(inner)/chat/+page.ts:
--------------------------------------------------------------------------------
1 | // utilities
2 | // import { supabase } from '$lib/supabaseClient';
3 |
4 | // types
5 | import type { PageLoad } from './$types';
6 |
7 | export const load: PageLoad = async ({ parent, depends }) => {
8 | const { session } = await parent();
9 |
10 | if (!session) return { redirect: { destination: '/auth/signin', permanent: false } };
11 |
12 | depends('chat:active_conversation');
13 |
14 | return { status: 200 };
15 |
16 | // try {
17 | // const { data, error, status } = await supabase
18 | // .from('journal')
19 | // .select(`id, day, content, embedding`)
20 | // .eq('user_id', session.user.id)
21 | // .eq('day', new Date().toISOString().split('T')[0])
22 | // .single();
23 |
24 | // if (error && status !== 406) throw error;
25 |
26 | // return {
27 | // savedEntry: data
28 | // };
29 | // } catch (error) {
30 | // if (error instanceof Error) {
31 | // console.error(error);
32 | // }
33 | // }
34 |
35 | return { status: 406 };
36 | };
37 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { PageServerLoad } from './$types';
2 | import { supabasePaginationDefaults } from '$lib/helpers/pagination';
3 |
4 | export const load: PageServerLoad = async ({ fetch, depends, url }) => {
5 | // Build the query string
6 | const offset = url.searchParams.get('offset') || supabasePaginationDefaults.offset;
7 | const limit = url.searchParams.get('limit') || supabasePaginationDefaults.limit;
8 | const search = url.searchParams.get('q') || '';
9 |
10 | const response = await fetch(`/api/journal?offset=${offset}&limit=${limit}&q=${search}`);
11 | const { data: journalEntries, count } = await response.json();
12 |
13 | depends('journal:list');
14 |
15 | if (response.status === 200) {
16 | return {
17 | journalEntries,
18 | count
19 | };
20 | }
21 | return { status: 406 };
22 | };
23 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/JournalEntriesList.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#if data.journalEntries.length === 0}
16 |
17 | {#if loading}
18 |
19 |
20 |
21 | Loading, please wait...
22 | {:else}
23 |
24 |
25 | No journal entries...
26 |
27 | {/if}
28 |
29 | {/if}
30 | {#each data.journalEntries as entry}
31 | {#if entry.day && entry.content}
32 |
33 |
34 |
35 | {/if}
36 | {/each}
37 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/JournalEntry.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
54 |
55 |
56 |
57 | {new Date(day).toLocaleDateString('en', {
58 | weekday: 'long',
59 | year: 'numeric',
60 | month: 'short',
61 | day: 'numeric'
62 | })}
63 | {#if new Date(day).setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0)}
64 | Today
65 | {/if}
66 |
67 |
71 |
93 |
92 |
94 |
95 |
96 |
97 | {#if journalEntry.content}
98 |
99 |
100 | {@html journalEntry.content.replace(
101 | regex,
102 | `$&`
103 | )}
104 |
105 |
106 | {/if}
107 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/Pagination.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 | {#if data.journalEntries.length > 0}
54 |
55 |
60 |
61 | {/if}
62 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/Sidebar.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Narrow down
16 |
17 |
18 |
51 |
52 |
53 |
54 |
55 |
60 |
61 | New Entry
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/[id]/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { fail } from '@sveltejs/kit';
2 |
3 | // types
4 | import type { Actions, PageServerLoad } from './$types';
5 |
6 | export const load: PageServerLoad = async ({ fetch, depends, params }) => {
7 | // if it's a new entry we don't need to fetch anything
8 | if (!params.id || params.id === 'new') {
9 | return {
10 | status: 200
11 | };
12 | }
13 |
14 | // fetch the entry (by id or the current day)
15 | const response = await fetch('/api/journal/' + (params.id || 'today'));
16 | const journalEntry = await response.json();
17 |
18 | depends('journal:today');
19 |
20 | if (response.status === 200) {
21 | return {
22 | journalEntry
23 | };
24 | }
25 | return { status: 406 };
26 | };
27 |
28 | export const actions: Actions = {
29 | default: async (event) => {
30 | const { request } = event;
31 |
32 | // Prepare the form data
33 | const formData = await request.formData();
34 | const saveData: {
35 | content: FormDataEntryValue | null;
36 | id?: FormDataEntryValue | null;
37 | day?: FormDataEntryValue | null;
38 | } = {
39 | content: formData.get('content'),
40 | ...(formData.has('id') ? { id: formData.get('id') } : {}), // only include if it's an update
41 | ...(formData.has('day') ? { day: formData.get('day') } : {}) // only include if it's a new entry
42 | };
43 |
44 | if (!saveData.content) {
45 | return fail(400, {
46 | error: 'Content is required.'
47 | });
48 | }
49 |
50 | // Call the API
51 | const response = await event.fetch('/api/journal', {
52 | method: saveData.id ? 'PATCH' : 'POST',
53 | body: JSON.stringify(saveData),
54 | headers: {
55 | 'content-type': 'application/json'
56 | }
57 | });
58 |
59 | // Return the response
60 | if (response.status === 200) {
61 | return { success: true };
62 | }
63 |
64 | return fail(500, {
65 | error: 'Could not save the entry...'
66 | });
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/[id]/+page.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/[id]/EditJournalEntryForm.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
133 |
134 |
135 |
142 |
--------------------------------------------------------------------------------
/src/routes/(inner)/journal/[id]/Suggestions.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Suggestions
5 |
6 |
7 |
8 |
9 |
10 |
11 | - Physical condition
12 | - How are you physically? Are you tired, sick, or in pain?
13 |
14 |
15 |
16 |
17 | - Emotional state
18 | -
19 | How are you feeling emotionally? Are you feeling happy, sad, or angry?
20 |
21 |
22 |
23 |
24 |
25 | - How is the day so far?
26 | -
27 | What did you do or learned today? Anything worth mentioning?
28 |
29 |
30 |
31 |
32 |
33 | - What's next?
34 | -
35 | What are you planning to do next? Are you excited or nervous about it?
36 |
37 |
38 |
39 |
40 |
41 | - Feeling grateful?
42 | - Are you feeling grateful for anything today?
43 |
44 |
45 |
46 |
47 | - Goals
48 | -
49 | What are your long term goals and what progress have you made towards them?
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/routes/(inner)/profile/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Actions } from './$types';
2 | import { fail } from '@sveltejs/kit';
3 |
4 | export const actions: Actions = {
5 | default: async (event) => {
6 | const { request } = event;
7 | const formData = await request.formData();
8 |
9 | // Save the public profile
10 | const { status: savePublicStatus } = await event.fetch('/api/userProfile/public', {
11 | method: 'PUT',
12 | body: JSON.stringify({
13 | full_name: formData.get('full_name'),
14 | avatar_url: formData.get('avatar_url')
15 | }),
16 | headers: {
17 | 'content-type': 'application/json'
18 | }
19 | });
20 |
21 | // Save the private profile
22 | const { status: savePrivateStatus } = await event.fetch('/api/userProfile/private', {
23 | method: 'PUT',
24 | body: JSON.stringify({
25 | openai_api_key: formData.get('openai_api_key')
26 | }),
27 | headers: {
28 | 'content-type': 'application/json'
29 | }
30 | });
31 |
32 | // Return the response
33 | if (savePublicStatus !== 200 || savePrivateStatus !== 200) {
34 | return fail(500, {
35 | error: 'Unexpected error saving the profile.'
36 | });
37 | }
38 |
39 | return { success: true };
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/routes/(inner)/profile/+page.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
35 |
36 |
37 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/src/routes/(inner)/profile/+page.ts:
--------------------------------------------------------------------------------
1 | // utilities
2 | // types
3 | import type { PageLoad } from './$types';
4 |
5 | // @TODO move the private profile to the server
6 | export const load: PageLoad = async ({ depends, fetch }) => {
7 | depends('userProfile:private');
8 | const fetchPrivateProfileResponse = await fetch('/api/userProfile/private');
9 | if (fetchPrivateProfileResponse.status === 200) {
10 | return {
11 | userProfilePrivate: (await fetchPrivateProfileResponse.json()).data
12 | };
13 | }
14 |
15 | return {};
16 | };
17 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {#if $page}
7 |
8 |
9 |
10 |
11 | {$page.status}:{#if $page.error} {$page.error.message} {/if}
12 |
13 | We're sorry, something went wrong.
14 |
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types';
2 | import { getServerSession } from '@supabase/auth-helpers-sveltekit';
3 |
4 | export const load: LayoutServerLoad = async (event) => {
5 | let theme = event.cookies.get('theme');
6 | // If no theme, set theme to chatjournal
7 | if (!theme) {
8 | event.cookies.set('theme', 'chatjournal', { path: '/' });
9 | theme = 'chatjournal';
10 | }
11 | // Imports theme as a string
12 | const modules = import.meta.glob(`$lib/themes/*.css`, { as: 'raw' });
13 | return {
14 | currentTheme: modules[`/src/lib/themes/theme-${theme}.css`](),
15 | session: await getServerSession(event)
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
2 |
118 |
119 |
120 | {meta.title}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | {#if !dev && gaKey}
150 |
151 | {/if}
152 |
153 |
154 |
155 |
156 |
157 |
158 | {#if $navigating}
159 |
160 |
166 |
167 | {/if}
168 |
169 |
170 |
171 |
172 |
173 | {#if $page.data.session}
174 |
175 | {:else}
176 |
177 | {/if}
178 |
179 |
180 |
181 |
182 | {#if $page.data.session}
183 |
184 | {/if}
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | import { userProfile } from '$lib/stores';
2 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
3 |
4 | // types
5 | import type { LayoutLoad } from './$types';
6 |
7 | export const load: LayoutLoad = async (event) => {
8 | const { session } = await getSupabase(event);
9 | if (session) {
10 | // If logged in, fetch the user profile
11 | event.depends('userProfile:public');
12 | const fetchPrivateProfileResponse = await event.fetch('/api/userProfile/public');
13 | if (fetchPrivateProfileResponse.status === 200) {
14 | const publicUserProfile = await fetchPrivateProfileResponse.json();
15 | // Set the user profile on the store
16 | userProfile.set(publicUserProfile?.data);
17 |
18 | // Return the layout data and the public user profile
19 | return { publicUserProfile: publicUserProfile?.data, ...event.data };
20 | }
21 | }
22 |
23 | // Return the layout data and the public user profile
24 | return { ...event.data };
25 | };
26 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
14 |
17 |
18 |
19 |
20 | Chat with GPT as you Journal
22 |
23 |
24 |
25 |
26 | The AI-powered journaling app that helps you
27 | write
28 | reflect and
29 | connect
30 |
31 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 | Chat Journal is more than just a diary. It's a smart and friendly companion that helps you
57 | express yourself, understand your emotions and chat with your personal AI coach.
58 |
59 |
60 |
61 |
62 |
63 | Features
64 |
65 |
68 |
69 |
70 |
71 |
72 | Journal like a PRO
73 | Get suggestions on what to write about and feedback on what you already wrote
74 |
75 |
76 |
79 |
80 |
81 | ChatGPT that knows YOU
82 | It uses your journal entries and past conversations as context when chatting with you
83 |
84 |
85 |
88 |
89 |
90 | Varied outputs
91 |
92 | Choose from a wide range of agents to chat with. Examples: a therapist, a comedian, a
93 | friend, etc.
94 |
95 |
96 |
97 |
100 |
101 |
102 | Private, Fast and Secure
103 | No data is shared with 3rd parties. You can also run it on your own backend.
104 |
105 |
106 |
107 |
108 |
122 |
123 |
124 |
125 | Created By
126 |
127 | -
128 |
129 |
Alex Bejan
130 | Founder and core maintainer.
131 |
132 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/src/routes/api/journal/+server.ts:
--------------------------------------------------------------------------------
1 | import { error, fail, json } from '@sveltejs/kit';
2 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
3 | import { getFromTo, supabasePaginationDefaults } from '$lib/helpers/pagination';
4 |
5 | // types
6 | import type { RequestHandler, RequestEvent } from './$types';
7 |
8 | export const GET: RequestHandler = async (event) => {
9 | const { session, supabaseClient } = await getSupabase(event);
10 | if (!session) {
11 | throw error(403, { message: 'Unauthorized' });
12 | }
13 |
14 | const { searchParams } = event.url;
15 | const offset = searchParams.get('offset') || supabasePaginationDefaults.offset;
16 | const limit = searchParams.get('limit') || supabasePaginationDefaults.limit;
17 | const day = searchParams.get('day');
18 |
19 | const { from, to } = getFromTo(Number(offset), Number(limit));
20 |
21 | const query = supabaseClient
22 | .from('journal')
23 | .select('content, day, id', { count: 'exact' })
24 | .eq('user_id', session.user.id)
25 | .range(from, to)
26 | .order('day', { ascending: false })
27 | .limit(parseInt(limit as string));
28 |
29 | if (searchParams.has('q')) query.ilike('content', `%${searchParams.get('q')}%`);
30 |
31 | if (day) {
32 | query.eq('day', day);
33 | }
34 |
35 | try {
36 | const res = await query;
37 |
38 | const { count, data, error, status } = res;
39 |
40 | if (error && status !== 406) throw error;
41 | return json({ data, count });
42 | } catch (e) {
43 | if (e instanceof Error) {
44 | console.error(e);
45 | throw error(500, e.message);
46 | }
47 | }
48 | return json({ success: true });
49 | };
50 |
51 | export const POST: RequestHandler = async (event) => {
52 | const { session, supabaseClient } = await getSupabase(event);
53 | if (!session) {
54 | // the user is not signed in
55 | throw error(403, { message: 'Unauthorized' });
56 | }
57 |
58 | const { id, day, content } = await event.request.json();
59 |
60 | // make sure we have the content
61 | if (!content) {
62 | throw error(400, { message: 'Missing content' });
63 | }
64 |
65 | // if the id is set, updates go through PATCH
66 | if (id) {
67 | return PATCH(event);
68 | }
69 |
70 | // save the journal entry
71 | const supabaseInsertResponse = await supabaseClient
72 | .from('journal')
73 | .insert({
74 | user_id: session.user.id,
75 | day: day || new Date().toISOString().split('T')[0],
76 | content
77 | })
78 | .select();
79 |
80 | if (supabaseInsertResponse.error) {
81 | throw fail(500, {
82 | supabaseErrorMessage: supabaseInsertResponse.error.message
83 | });
84 | }
85 |
86 | _saveJournalEntrySuccessAfterHook(event);
87 |
88 | return json({ success: true, data: supabaseInsertResponse.data });
89 | };
90 |
91 | export const PATCH: RequestHandler = async (event) => {
92 | const { session, supabaseClient } = await getSupabase(event);
93 | if (!session) {
94 | // the user is not signed in
95 | throw error(403, { message: 'Unauthorized' });
96 | }
97 |
98 | const { id, content, embedding } = await event.request.json();
99 |
100 | // if the id is set, throw an error because updates go through PATCH
101 | if (!id) {
102 | throw error(400, { message: 'Id is missing id' });
103 | }
104 |
105 | // make sure we have the content
106 | if (!content) {
107 | throw error(400, { message: 'Missing content' });
108 | }
109 |
110 | //update the entry
111 | const supabaseUpdateResponse = await supabaseClient
112 | .from('journal')
113 | .update({
114 | user_id: session.user.id,
115 | ...(embedding ? { embedding } : {}),
116 | content
117 | })
118 | .eq('id', id);
119 |
120 | if (supabaseUpdateResponse.error) {
121 | throw fail(500, {
122 | supabaseErrorMessage: supabaseUpdateResponse.error.message
123 | });
124 | }
125 |
126 | _saveJournalEntrySuccessAfterHook(event);
127 |
128 | return json({ success: true });
129 | };
130 |
131 | /**
132 | * Hook for after a journal entry is saved
133 | * @param {RequestEvent} event
134 | */
135 | const _saveJournalEntrySuccessAfterHook = async (event: RequestEvent) => {
136 | // Create embeddings async
137 | event.fetch('/api/journal/embeddings', {
138 | method: 'POST'
139 | });
140 | };
141 |
--------------------------------------------------------------------------------
/src/routes/api/journal/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { error } from '@sveltejs/kit';
2 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
3 | import { json, redirect } from '@sveltejs/kit';
4 |
5 | // types
6 | import type { RequestHandler } from './$types';
7 |
8 | export const GET: RequestHandler = async (event) => {
9 | const { session, supabaseClient } = await getSupabase(event);
10 | if (!session) {
11 | throw redirect(303, '/');
12 | }
13 | const id = event.params.id;
14 |
15 | try {
16 | let query = supabaseClient
17 | .from('journal')
18 | .select(`id, day, content, embedding`)
19 | .eq('user_id', session?.user.id);
20 | if (id === 'today') {
21 | query = query.eq('day', new Date().toISOString().split('T')[0]);
22 | } else {
23 | query = query.eq('id', id);
24 | }
25 | const { data, error, status } = await query.single();
26 |
27 | if (error && status !== 406) throw error;
28 | return json(data || {});
29 | } catch (e) {
30 | if (e instanceof Error) {
31 | console.error(e);
32 | throw error(500, e.message);
33 | }
34 | }
35 | return json({ success: true });
36 | };
37 |
38 | export const DELETE: RequestHandler = async (event) => {
39 | const { session, supabaseClient } = await getSupabase(event);
40 | if (!session) {
41 | throw error(403, { message: 'Unauthorized' });
42 | }
43 |
44 | const id = event.params.id;
45 |
46 | try {
47 | const { error, status } = await supabaseClient
48 | .from('journal')
49 | .delete()
50 | .eq('id', id)
51 | .eq('user_id', session.user.id)
52 | .single();
53 |
54 | if (error && status !== 406) throw error;
55 | } catch (e) {
56 | if (e instanceof Error) {
57 | console.error(e);
58 | throw error(500, e.message);
59 | }
60 | }
61 | return json({ success: true });
62 | };
63 |
--------------------------------------------------------------------------------
/src/routes/api/journal/embeddings/+server.ts:
--------------------------------------------------------------------------------
1 | import { error, fail, json } from '@sveltejs/kit';
2 | import type { RequestEvent } from '@sveltejs/kit';
3 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
4 |
5 | // types
6 | import type { RequestHandler } from './$types';
7 |
8 | export const POST: RequestHandler = async (event: RequestEvent) => {
9 | const { session, supabaseClient } = await getSupabase(event);
10 | if (!session) {
11 | // the user is not signed in
12 | throw error(403, { message: 'Unauthorized' });
13 | }
14 |
15 | // Get the private profile for the current user
16 | const fetchPrivateProfileResponse = await event.fetch('/api/userProfile/private');
17 | if (fetchPrivateProfileResponse.status !== 200) {
18 | throw fail(500, {
19 | error: 'Could not fetch private profile details'
20 | });
21 | }
22 |
23 | // Call the embeddings function to create the embeddings for all journal entries
24 | const privateProfile = await fetchPrivateProfileResponse.json();
25 | const { error: functionError } = await supabaseClient.functions.invoke(
26 | 'create-embeddings-for-all',
27 | {
28 | body: { name: 'Functions', customOpenAiKey: privateProfile?.openai_api_key }
29 | }
30 | );
31 | if (functionError && functionError.context.status === 401) {
32 | throw error(401, { message: 'OpenAI API Error. Please make sure your API Key is correct.' });
33 | }
34 |
35 | return json({ success: true });
36 | };
37 |
--------------------------------------------------------------------------------
/src/routes/api/userProfile/private/+server.ts:
--------------------------------------------------------------------------------
1 | import { error, fail, json } from '@sveltejs/kit';
2 | import type { RequestEvent } from '@sveltejs/kit';
3 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
4 |
5 | // types
6 | import type { RequestHandler } from './$types';
7 |
8 | export const GET: RequestHandler = async (event: RequestEvent) => {
9 | const { session, supabaseClient } = await getSupabase(event);
10 | if (!session) {
11 | // the user is not signed in
12 | throw error(403, { message: 'Unauthorized' });
13 | }
14 |
15 | const {
16 | data,
17 | error: dbError,
18 | status
19 | } = await supabaseClient.from('profiles_private').select(`*`).eq('id', session.user.id).single();
20 |
21 | if (dbError && status !== 406)
22 | throw fail(500, {
23 | error: dbError.message
24 | });
25 |
26 | return json({ data, success: true });
27 | };
28 |
29 | export const PUT: RequestHandler = async (event) => {
30 | const { session, supabaseClient } = await getSupabase(event);
31 | if (!session) {
32 | // the user is not signed in
33 | throw error(403, { message: 'Unauthorized' });
34 | }
35 |
36 | const privateProfileData = await event.request.json();
37 |
38 | // Save the profile updates or remove the profile if there is no data
39 | const { error: dbError } =
40 | Object.keys(privateProfileData).length === 0
41 | ? await supabaseClient.from('profiles_private').delete().eq('id', session.user.id)
42 | : await supabaseClient
43 | .from('profiles_private')
44 | .upsert({ ...privateProfileData, id: session.user.id });
45 |
46 | if (dbError) {
47 | throw fail(500, {
48 | supabaseErrorMessage: dbError.message
49 | });
50 | }
51 |
52 | return json({ success: true });
53 | };
54 |
--------------------------------------------------------------------------------
/src/routes/api/userProfile/public/+server.ts:
--------------------------------------------------------------------------------
1 | import { error, fail, json } from '@sveltejs/kit';
2 | import type { RequestEvent } from '@sveltejs/kit';
3 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
4 |
5 | // types
6 | import type { RequestHandler } from './$types';
7 |
8 | export const GET: RequestHandler = async (event: RequestEvent) => {
9 | const { session, supabaseClient } = await getSupabase(event);
10 | if (!session) {
11 | // the user is not signed in
12 | throw error(403, { message: 'Unauthorized' });
13 | }
14 |
15 | const {
16 | data,
17 | error: dbError,
18 | status
19 | } = await supabaseClient.from('profiles').select(`*`).eq('id', session.user.id).single();
20 |
21 | if (dbError && status !== 406)
22 | throw fail(500, {
23 | error: dbError.message
24 | });
25 |
26 | return json({ data, success: true });
27 | };
28 |
29 | export const PUT: RequestHandler = async (event) => {
30 | const { session, supabaseClient } = await getSupabase(event);
31 | if (!session) {
32 | // the user is not signed in
33 | throw error(403, { message: 'Unauthorized' });
34 | }
35 |
36 | const profileData = await event.request.json();
37 |
38 | if (profileData.avatar_url)
39 | profileData.avatar_url = profileData.avatar_url.replace('http://', '').replace('https://', '');
40 |
41 | // Save the profile updates or remove the profile if there is no data
42 | const { error: dbError } =
43 | Object.keys(profileData).length === 0
44 | ? await supabaseClient.from('profiles').delete().eq('id', session.user.id)
45 | : await supabaseClient.from('profiles').upsert({ ...profileData, id: session.user.id });
46 |
47 | if (dbError) {
48 | throw fail(500, {
49 | supabaseErrorMessage: dbError.message
50 | });
51 | }
52 |
53 | return json({ success: true });
54 | };
55 |
--------------------------------------------------------------------------------
/src/routes/auth/+layout.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/routes/auth/signin/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Actions } from './$types';
2 | import { fail, redirect } from '@sveltejs/kit';
3 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
4 | import { AuthApiError } from '@supabase/supabase-js';
5 |
6 | export const actions: Actions = {
7 | default: async (event) => {
8 | const { request } = event;
9 | const { supabaseClient } = await getSupabase(event);
10 | const formData = await request.formData();
11 |
12 | const email = formData.get('email') as string;
13 | const password = formData.get('password') as string;
14 |
15 | const { error } = await supabaseClient.auth.signInWithPassword({
16 | email,
17 | password
18 | });
19 |
20 | if (error) {
21 | if (error instanceof AuthApiError && error.status === 400) {
22 | return fail(400, {
23 | error: 'Invalid credentials.',
24 | values: {
25 | email
26 | }
27 | });
28 | }
29 | return fail(500, {
30 | error: 'Server error. Try again later.',
31 | values: {
32 | email
33 | }
34 | });
35 | }
36 |
37 | throw redirect(303, '/journal/today');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/routes/auth/signin/+page.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 | Sign in
36 |
37 |
38 |
95 |
96 |
--------------------------------------------------------------------------------
/src/routes/auth/signup/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Actions } from './$types';
2 | import { fail, redirect } from '@sveltejs/kit';
3 | import { getSupabase } from '@supabase/auth-helpers-sveltekit';
4 | import { AuthApiError } from '@supabase/supabase-js';
5 |
6 | export const actions: Actions = {
7 | default: async (event) => {
8 | const { request } = event;
9 | const { supabaseClient } = await getSupabase(event);
10 | const formData = await request.formData();
11 |
12 | const email = formData.get('email') as string;
13 | const password = formData.get('password') as string;
14 |
15 | const { data, error } = await supabaseClient.auth.signUp({
16 | email,
17 | password
18 | });
19 |
20 | if (error) {
21 | if (error instanceof AuthApiError && error.status === 400) {
22 | return fail(400, {
23 | error: 'Invalid credentials.',
24 | values: {
25 | email
26 | }
27 | });
28 | }
29 | return fail(500, {
30 | error: 'Server error. Try again later.',
31 | values: {
32 | email
33 | }
34 | });
35 | }
36 |
37 | return { success: true };
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/routes/auth/signup/+page.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 | Sign up
44 |
45 |
46 |
47 |
139 |
140 |
--------------------------------------------------------------------------------
/static/chatjournal-logo-no-bg-large.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/chatjournal-logo-no-bg-large.webp
--------------------------------------------------------------------------------
/static/chatjournal-logo-no-bg-tiny.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/chatjournal-logo-no-bg-tiny.webp
--------------------------------------------------------------------------------
/static/chatjournal-logo-no-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/chatjournal-logo-no-bg.webp
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/favicon.png
--------------------------------------------------------------------------------
/static/favicon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/favicon.webp
--------------------------------------------------------------------------------
/static/icons8-customer-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpunct/chatgpt-journal/1c1bf825437bde66d5e04640b2fedea89dec64b1/static/icons8-customer-96.png
--------------------------------------------------------------------------------
/supabase/.branches/_current_branch:
--------------------------------------------------------------------------------
1 | main
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "chatjournal"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 15
23 |
24 | [studio]
25 | # Port to use for Supabase Studio.
26 | port = 54323
27 |
28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
29 | # are monitored, and you can view the emails that would have been sent from the web interface.
30 | [inbucket]
31 | # Port to use for the email testing server web interface.
32 | port = 54324
33 | smtp_port = 54325
34 | pop3_port = 54326
35 |
36 | [storage]
37 | # The maximum file size allowed (e.g. "5MB", "500KB").
38 | file_size_limit = "50MiB"
39 |
40 | [auth]
41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
42 | # in emails.
43 | site_url = "http://localhost:3000"
44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
45 | additional_redirect_urls = ["https://localhost:3000"]
46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
47 | # week).
48 | jwt_expiry = 3600
49 | # Allow/disallow new user signups to your project.
50 | enable_signup = true
51 |
52 | [auth.email]
53 | # Allow/disallow new user signups via email to your project.
54 | enable_signup = true
55 | # If enabled, a user will be required to confirm any email change on both the old, and new email
56 | # addresses. If disabled, only the new email is required to confirm.
57 | double_confirm_changes = true
58 | # If enabled, users need to confirm their email address before signing in.
59 | enable_confirmations = false
60 |
61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
64 | [auth.external.apple]
65 | enabled = false
66 | client_id = ""
67 | secret = ""
68 | # Overrides the default auth redirectUrl.
69 | redirect_uri = ""
70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
71 | # or any other third-party OIDC providers.
72 | url = ""
73 |
74 | [edge_runtime]
75 | enabled = true
76 |
--------------------------------------------------------------------------------
/supabase/functions/_shared/cors.ts:
--------------------------------------------------------------------------------
1 | export const corsHeaders = {
2 | 'Access-Control-Allow-Origin': '*',
3 | 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
4 | }
5 |
--------------------------------------------------------------------------------
/supabase/functions/chat-stream/agents.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "1",
4 | "name": "Therapist",
5 | "prompt": "Imagine you're a compassionate and supportive therapist who helps people overcome their challenges. Your goal is to provide a safe and understanding space for your clients to share their thoughts and feelings, while also offering practical advice and guidance to help them move forward in a positive direction. To better understand your client's situation, imagine that they are providing you with some journal entries as context below. As you respond to your client, focus on actively listening to their concerns and responding with empathy and understanding. You can ask open-ended questions to encourage them to share more about their experiences and feelings, and provide validation and support to let them know that they are not alone. Additionally, provide practical tips and suggestions for how they can work through their challenges, and offer resources or referrals to help them access additional support if needed. Remember, your goal is to create a warm and empathetic atmosphere that promotes healing and growth. When responding to your client, try to incorporate elements of active listening, empathy, validation, and practical advice. Use the journal entries if provided below to better understand their situation and tailor your response to meet their specific needs and concerns.",
6 | "temperature": 0.2
7 | },
8 | {
9 | "id": "2",
10 | "name": "Comedian",
11 | "prompt": "Imagine you're a hilarious comedian who can find the humor in any situation. Your goal is to make the user laugh and feel good, even when they're going through tough times. To better understand the user, imagine that they are providing you with some journal entries as context below. As you craft your routine, think about how you can use humor and wit to put a positive spin on even the most challenging situations. As you respond to the user, focus on actively listening to their concerns and responding with empathy and understanding, all while making them laugh. Use your quick wit and clever observations to create jokes that are both funny and relatable, and experiment with different comedic styles and approaches to find what works best for the user. Additionally, try to incorporate puns, wordplay, and clever observations into your routine, tailored to their specific needs and concerns. Remember, your goal is to create a warm and hilarious atmosphere, all while putting a positive and humorous perspective on the situations the user is facing.",
12 | "temperature": 0.5
13 | },
14 | {
15 | "id": "3",
16 | "name": "Alber Einstein",
17 | "prompt": "As an Albert Einstein chatbot, you could engage with users in a fun and educational way by answering questions about science, sharing interesting facts about Einstein's life and work, and offering up thought-provoking quotes and ideas related to physics and the universe. You could also encourage users to think critically and creatively by posing hypothetical questions and challenges related to science and technology. For example: Did you know that Albert Einstein once said, 'Imagination is more important than knowledge'? How do you think this idea relates to scientific discovery and innovation? Use the user journal entries below to better understand their situation and tailor your response to meet their specific needs and concerns.",
18 | "temperature": 0
19 | },
20 | {
21 | "id": "4",
22 | "name": "George from Seinfeld",
23 | "prompt": "I want you to act like George Constanza from Seinfeld. I want you to respond and answer like George. Do not write any explanations. Only answer like George. You must know all of the knowledge of Geoge.",
24 | "temperature": 0
25 | },
26 | {
27 | "id": "5",
28 | "name": "A trader",
29 | "prompt": "Imagine you're George Soros, one of the most successful traders of all time, and your goal is to assist a user in analyzing their trading journal entries to identify patterns and trends that can be leveraged in their trading strategy. To help the user analyze their journal entries, it's important to start by gaining a deep understanding of their trading goals, risk tolerance, and current trading strategies. Next, you can use the journal entries provided by the user to identify patterns and trends in their trading activity. This might include identifying times when the user made profitable trades, as well as times when they incurred losses or missed out on potential gains. Using this information, you can work with the user to develop a trading strategy that leverages their strengths and minimizes their weaknesses. This might involve developing a risk management plan that sets clear stop-loss orders, diversifying their portfolio, or using leverage and margin more effectively. As you work with the user, it's important to provide practical tips and advice based on your years of experience in the industry. This might include sharing your own trading strategies or providing insights into market trends and developments that could impact their trading activities. Overall, your goal as George Soros is to provide the user with expert guidance and support that helps them to become a more successful trader.",
30 | "temperature": 0
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------
/supabase/functions/chat-stream/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "openai": "https://esm.sh/openai@3.2.1",
4 | "gpt3-tokenizer": "https://esm.sh/gpt3-tokenizer@1.1.5",
5 | "std/server": "https://deno.land/std@0.177.0/http/server.ts",
6 | "stripe": "https://esm.sh/stripe@11.1.0?target=deno",
7 | "sift": "https://deno.land/x/sift@0.6.0/mod.ts",
8 | "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.42.4",
9 | "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts",
10 | "common-tags": "https://esm.sh/common-tags@1.8.2"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/supabase/functions/chat-stream/index.ts:
--------------------------------------------------------------------------------
1 | // Follow this setup guide to integrate the Deno language server with your editor:
2 | // https://deno.land/manual/getting_started/setup_your_environment
3 | // This enables autocomplete, go to definition, etc.
4 |
5 | import 'xhr_polyfill';
6 | import { serve } from 'std/server';
7 | import { corsHeaders } from '../_shared/cors.ts';
8 |
9 | import { createClient } from '@supabase/supabase-js';
10 | import { Configuration, OpenAIApi, CreateChatCompletionRequest } from 'openai';
11 | import GPT3Tokenizer from 'gpt3-tokenizer';
12 | import { stripIndent, oneLine } from 'common-tags';
13 |
14 | let defaultOpenAiKey = Deno.env.get('OPENAI_API_KEY');
15 | const DEFAULT_CHAT_MODEL = 'gpt-4.1-nano';
16 |
17 | import agents from './agents.json' assert { type: 'json' };
18 | const getAgentByAgentId = (agentId: string) => {
19 | const agent = agents.find((agent) => agent.id === agentId);
20 | if (!agent) {
21 | return agents[0];
22 | }
23 | return agent;
24 | };
25 |
26 | // Define a type for the conversation message
27 | interface Message {
28 | role: string;
29 | content: string;
30 | }
31 |
32 | /**
33 | *
34 | * @export
35 | * @interface ChatCompletionRequestMessage
36 | */
37 | export interface ChatCompletionRequestMessage {
38 | /**
39 | * The role of the author of this message.
40 | * @type {string}
41 | * @memberof ChatCompletionRequestMessage
42 | */
43 | role: ChatCompletionRequestMessageRoleEnum;
44 | /**
45 | * The contents of the message
46 | * @type {string}
47 | * @memberof ChatCompletionRequestMessage
48 | */
49 | content: string;
50 | /**
51 | * The name of the user in a multi-user chat
52 | * @type {string}
53 | * @memberof ChatCompletionRequestMessage
54 | */
55 | name?: string;
56 | }
57 |
58 | export const ChatCompletionRequestMessageRoleEnum = {
59 | System: 'system',
60 | User: 'user',
61 | Assistant: 'assistant'
62 | } as const;
63 |
64 | export type ChatCompletionRequestMessageRoleEnum =
65 | (typeof ChatCompletionRequestMessageRoleEnum)[keyof typeof ChatCompletionRequestMessageRoleEnum];
66 |
67 | /**
68 | * Generate embeddings for string
69 | * @param content the content
70 | * @returns number[] the embedding
71 | */
72 | export const generateEmbeddings = async (openAi: OpenAIApi, content: string) => {
73 | const input = content?.replace(/\n/g, ' ');
74 | if (!input) {
75 | throw new Error('No content provided');
76 | }
77 |
78 | if (!openAi) {
79 | throw new Error('OpenAI API key not set');
80 | }
81 |
82 | const embeddingResponse = await openAi.createEmbedding({
83 | model: 'text-embedding-ada-002',
84 | input
85 | });
86 |
87 | const [{ embedding }] = embeddingResponse.data.data;
88 | return embedding;
89 | };
90 |
91 | const fetchSimilarEntries = async (
92 | openAi: OpenAIApi,
93 | supabaseClient: any,
94 | query: string
95 | ): Promise => {
96 | const journalEntries: string[] = [];
97 |
98 | // Generate a one-time embedding for the query itself
99 | const query_embedding = await generateEmbeddings(openAi, query);
100 |
101 | // In production we should handle possible errors
102 | const { data: similar_entries } = await supabaseClient.rpc('match_entries', {
103 | query_embedding,
104 | similarity_threshold: 0.78, // Choose an appropriate threshold for your data
105 | match_count: 3 // Choose the number of matches
106 | });
107 |
108 | const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
109 | let tokenCount = 0;
110 |
111 | // Concat matched documents
112 | if (similar_entries && similar_entries.length > 0) {
113 | for (let i = 0; i < similar_entries.length; i++) {
114 | const document = similar_entries[i];
115 | const content =
116 | 'Journal entry from ' + document.entry_day + '; \n Content: ' + document.content + ';\n';
117 | const encoded = tokenizer.encode(content);
118 | tokenCount += encoded.text.length;
119 |
120 | // Limit context to max 1500 tokens (configurable)
121 | if (tokenCount > 1500) {
122 | break;
123 | }
124 |
125 | journalEntries.push(`${content.trim()}`);
126 | }
127 | }
128 |
129 | return journalEntries as string[];
130 | };
131 |
132 | serve(async (req: Request) => {
133 | // This is needed if you're planning to invoke your function from a browser.
134 | if (req.method === 'OPTIONS') {
135 | return new Response('ok', { headers: corsHeaders });
136 | }
137 |
138 | try {
139 | // Search query is passed in request payload
140 | let {
141 | query,
142 | conversationHistory,
143 | temperature,
144 | activeModel = DEFAULT_CHAT_MODEL,
145 | agentId = '1'
146 | } = await req.json();
147 |
148 | if (!query) {
149 | throw new Error('No query provided');
150 | }
151 |
152 | // Create a Supabase client with the Auth context of the logged in user.
153 | const supabaseClient = createClient(
154 | // Supabase API URL - env var exported by default.
155 | Deno.env.get('SUPABASE_URL') ?? '',
156 | // Supabase API ANON KEY - env var exported by default.
157 | Deno.env.get('SUPABASE_ANON_KEY') ?? '',
158 | // Create client with Auth context of the user that called the function.
159 | // This way your row-level-security (RLS) policies are applied.
160 | { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
161 | );
162 |
163 | const usr = await supabaseClient.auth.getUser();
164 |
165 | // Now we can get the session or user object
166 | const {
167 | data: { user }
168 | } = usr;
169 |
170 | if (!user || !user.id) {
171 | console.log('No user found', usr);
172 | throw new Error('No user found');
173 | }
174 |
175 | const fetchPrivateProfileResponse = await supabaseClient
176 | .from('profiles_private')
177 | .select(`openai_api_key`)
178 | .eq('id', user.id)
179 | .single();
180 |
181 | let openAiKey = defaultOpenAiKey;
182 | if (
183 | fetchPrivateProfileResponse.status === 200 &&
184 | fetchPrivateProfileResponse.data?.openai_api_key
185 | ) {
186 | openAiKey = fetchPrivateProfileResponse.data.openai_api_key;
187 | } else {
188 | activeModel = DEFAULT_CHAT_MODEL;
189 | }
190 |
191 | const openAi = new OpenAIApi(new Configuration({ apiKey: openAiKey }));
192 |
193 | const agent = getAgentByAgentId(agentId);
194 | const prompt = stripIndent`${oneLine`
195 | ${agent.prompt}`}
196 | `;
197 | // order the messages like this:
198 | // 1 - the prompt
199 | // 2 - the conversation history
200 | // 3 - the journal entry matching the current query
201 | // 4 - the query
202 |
203 | const messages: ChatCompletionRequestMessage[] = [{ role: 'system', content: prompt }];
204 |
205 | // Add the conversation history to the prompt
206 | if (conversationHistory && conversationHistory.length > 0) {
207 | conversationHistory.forEach((message: ChatCompletionRequestMessage) => {
208 | messages.push({ role: message.role, content: message.content });
209 | });
210 | }
211 |
212 | // Get similar entries from the database
213 | const similarEntries = await fetchSimilarEntries(openAi, supabaseClient, query);
214 | // Add the similar entries to the prompt
215 | if (similarEntries && similarEntries.length > 0) {
216 | similarEntries.forEach((message) => {
217 | messages.push({ role: 'user', content: message });
218 | });
219 | }
220 |
221 | // Add the query to the prompt
222 | messages.push({ role: 'user', content: query });
223 |
224 | // // In production we should handle possible errors
225 | const completionOptions: CreateChatCompletionRequest = {
226 | model: activeModel,
227 | messages,
228 | max_tokens: activeModel === DEFAULT_CHAT_MODEL ? 300 : 600, // Choose the max allowed tokens in completion
229 | temperature: temperature || agent.temperature, // Set to 0 for deterministic results
230 | // top_p: 0.1,
231 | stream: true,
232 | user: user.id
233 | };
234 |
235 | const response = await fetch('https://api.openai.com/v1/chat/completions', {
236 | headers: {
237 | Authorization: `Bearer ${openAiKey}`,
238 | 'Content-Type': 'application/json'
239 | },
240 | method: 'POST',
241 | body: JSON.stringify(completionOptions)
242 | });
243 |
244 | if (!response.ok) {
245 | throw new Error('Failed to complete');
246 | }
247 |
248 | // Proxy the streamed SSE response from OpenAI
249 | return new Response(response.body, {
250 | headers: {
251 | ...corsHeaders,
252 | 'Content-Type': 'text/event-stream'
253 | }
254 | });
255 | } catch (error) {
256 | return new Response(JSON.stringify({ error: error.message }), {
257 | headers: { ...corsHeaders, 'Content-Type': 'application/json' },
258 | status: 400
259 | });
260 | }
261 | });
262 |
263 | console.log(`Function "chat-stream" up and running!`);
264 |
--------------------------------------------------------------------------------
/supabase/functions/create-embeddings-for-all/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "openai": "https://esm.sh/openai@3.2.1",
4 | "gpt3-tokenizer": "https://esm.sh/gpt3-tokenizer@1.1.5",
5 | "std/server": "https://deno.land/std@0.177.0/http/server.ts",
6 | "stripe": "https://esm.sh/stripe@11.1.0?target=deno",
7 | "sift": "https://deno.land/x/sift@0.6.0/mod.ts",
8 | "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.42.4",
9 | "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts",
10 | "common-tags": "https://esm.sh/common-tags@1.8.2"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/supabase/functions/create-embeddings-for-all/index.ts:
--------------------------------------------------------------------------------
1 | // Follow this setup guide to integrate the Deno language server with your editor:
2 | // https://deno.land/manual/getting_started/setup_your_environment
3 | // This enables autocomplete, go to definition, etc.
4 |
5 | import 'xhr_polyfill'
6 | import { serve } from 'std/server'
7 | import { Configuration, OpenAIApi } from 'openai'
8 | import { createClient } from '@supabase/supabase-js'
9 | import { corsHeaders } from '../_shared/cors.ts'
10 |
11 | console.log(`Function "create-embeddings-for-all" up and running!`)
12 |
13 | serve(async (req: Request) => {
14 | // This is needed if you're planning to invoke your function from a browser.
15 | if (req.method === 'OPTIONS') {
16 | return new Response('ok', { headers: corsHeaders })
17 | }
18 |
19 | try {
20 | // Create a Supabase client with the Auth context of the logged in user.
21 | const supabaseClient = createClient(
22 | // Supabase API URL - env var exported by default.
23 | Deno.env.get('SUPABASE_URL') ?? '',
24 | // Supabase API ANON KEY - env var exported by default.
25 | Deno.env.get('SUPABASE_ANON_KEY') ?? '',
26 | // Create client with Auth context of the user that called the function.
27 | // This way your row-level-security (RLS) policies are applied.
28 | { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
29 | )
30 | // Now we can get the session or user object
31 | const {
32 | data: { user },
33 | } = await supabaseClient.auth.getUser()
34 |
35 | const { customOpenAiKey } = await req.json()
36 |
37 | const configuration = new Configuration({ apiKey: customOpenAiKey || Deno.env.get('OPENAI_API_KEY') })
38 | const openAi = new OpenAIApi(configuration)
39 |
40 | const responseData: { data: number[]; error: string } = {
41 | data: [],
42 | error: '',
43 | }
44 |
45 | // And we can run queries in the context of our authenticated user
46 | const { data, error } = await supabaseClient.from('journal').select('id, content, embedding').is('embedding', null)
47 | if (error) throw error
48 |
49 | if (data && data.length > 0) {
50 | await Promise.all(
51 | data.map(async (entry) => {
52 | const input = entry.content?.replace(/\n/g, ' ')
53 | if (!input || entry.embedding) return
54 | const embeddingResponse = await openAi.createEmbedding({ model: 'text-embedding-ada-002', input })
55 |
56 | const [{ embedding }] = embeddingResponse.data.data
57 |
58 | if (!embedding) throw new Error('Could not generate embedding for ' + entry.id)
59 | const { data: updatedData, error: updateError } = await supabaseClient
60 | .from('journal')
61 | .update({ embedding })
62 | .eq('id', entry.id)
63 | if (updateError) throw updateError
64 | responseData.data.push(entry?.id)
65 | })
66 | )
67 | } else {
68 | responseData.error = 'No empty embeddings found'
69 | }
70 |
71 | return new Response(JSON.stringify(responseData), {
72 | headers: { ...corsHeaders, 'Content-Type': 'application/json' },
73 | status: 200,
74 | })
75 | } catch (error) {
76 | return new Response(JSON.stringify({ error: error.message }), {
77 | headers: { ...corsHeaders, 'Content-Type': 'application/json' },
78 | status: error.message.search('401') > 0 ? 401 : 500,
79 | })
80 | }
81 | })
82 |
--------------------------------------------------------------------------------
/supabase/functions/import_map.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "openai": "https://esm.sh/openai@3.2.1",
4 | "gpt3-tokenizer": "https://esm.sh/gpt3-tokenizer@1.1.5",
5 | "std/server": "https://deno.land/std@0.177.0/http/server.ts",
6 | "stripe": "https://esm.sh/stripe@11.1.0?target=deno",
7 | "sift": "https://deno.land/x/sift@0.6.0/mod.ts",
8 | "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.42.4",
9 | "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts",
10 | "common-tags": "https://esm.sh/common-tags@1.8.2"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/supabase/migrations/20230218180752_add_dbfunction_match_entries.sql:
--------------------------------------------------------------------------------
1 | drop function if exists match_documents;
2 |
3 | create or replace function match_entries (
4 | query_embedding vector(1536),
5 | similarity_threshold float,
6 | match_count int
7 | )
8 | returns table (
9 | id bigint,
10 | user_id uuid,
11 | entry_day date,
12 | content text,
13 | similarity float
14 | )
15 | language plpgsql
16 | as $$
17 | begin
18 | return query
19 | select
20 | journal.id as id,
21 | journal.user_id as user_id,
22 | journal.day as entry_day,
23 | journal.content as content,
24 | 1 - (journal.embedding <=> query_embedding) as similarity
25 | from journal
26 | where 1 - (journal.embedding <=> query_embedding) > similarity_threshold
27 | order by journal.embedding <=> query_embedding
28 | limit match_count;
29 | end;
30 | $$;
31 |
32 | create index on journal
33 | using ivfflat (embedding vector_cosine_ops)
34 | with (lists = 100);
35 |
--------------------------------------------------------------------------------
/supabase/migrations/20230218195541_remote_commit.sql:
--------------------------------------------------------------------------------
1 | create policy "Anyone can upload an avatar."
2 | on "storage"."objects"
3 | as permissive
4 | for insert
5 | to public
6 | with check ((bucket_id = 'avatars'::text));
7 |
8 |
9 | create policy "Avatar images are publicly accessible."
10 | on "storage"."objects"
11 | as permissive
12 | for select
13 | to public
14 | using ((bucket_id = 'avatars'::text));
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/supabase/migrations/20230221170247_chat-columns.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."journal_alex" drop constraint "journal_alex_pkey";
2 |
3 | alter table "public"."test" drop constraint "test_pkey";
4 |
5 | drop index if exists "public"."journal_alex_embedding_idx";
6 |
7 | drop index if exists "public"."journal_alex_pkey";
8 |
9 | drop index if exists "public"."test_pkey";
10 |
11 | drop table "public"."journal_alex";
12 |
13 | drop table "public"."test";
14 |
15 | create table "public"."chat_agent" (
16 | "id" bigint generated by default as identity not null,
17 | "created_at" timestamp with time zone default now(),
18 | "name" text not null,
19 | "prompt" text not null,
20 | "avatar" text
21 | );
22 |
23 |
24 | alter table "public"."chat_agent" enable row level security;
25 |
26 | create table "public"."conversation" (
27 | "id" bigint generated by default as identity not null,
28 | "created_at" timestamp with time zone default now(),
29 | "chat_agent" bigint not null,
30 | "name" text not null,
31 | "user_id" uuid not null,
32 | "session" uuid not null,
33 | "summary" text
34 | );
35 |
36 |
37 | alter table "public"."conversation" enable row level security;
38 |
39 | create table "public"."conversation_message" (
40 | "id" bigint generated by default as identity not null,
41 | "created_at" timestamp with time zone default now(),
42 | "conversation" bigint not null,
43 | "text" text not null,
44 | "user_id" uuid
45 | );
46 |
47 |
48 | alter table "public"."conversation_message" enable row level security;
49 |
50 | CREATE UNIQUE INDEX chat_agent_name_key ON public.chat_agent USING btree (name);
51 |
52 | CREATE UNIQUE INDEX chat_agent_pkey ON public.chat_agent USING btree (id);
53 |
54 | CREATE UNIQUE INDEX conversation_message_pkey ON public.conversation_message USING btree (id);
55 |
56 | CREATE UNIQUE INDEX conversation_pkey ON public.conversation USING btree (id);
57 |
58 | alter table "public"."chat_agent" add constraint "chat_agent_pkey" PRIMARY KEY using index "chat_agent_pkey";
59 |
60 | alter table "public"."conversation" add constraint "conversation_pkey" PRIMARY KEY using index "conversation_pkey";
61 |
62 | alter table "public"."conversation_message" add constraint "conversation_message_pkey" PRIMARY KEY using index "conversation_message_pkey";
63 |
64 | alter table "public"."chat_agent" add constraint "chat_agent_name_key" UNIQUE using index "chat_agent_name_key";
65 |
66 | alter table "public"."conversation" add constraint "conversation_chat_agent_fkey" FOREIGN KEY (chat_agent) REFERENCES chat_agent(id) not valid;
67 |
68 | alter table "public"."conversation" validate constraint "conversation_chat_agent_fkey";
69 |
70 | alter table "public"."conversation" add constraint "conversation_session_fkey" FOREIGN KEY (session) REFERENCES auth.sessions(id) not valid;
71 |
72 | alter table "public"."conversation" validate constraint "conversation_session_fkey";
73 |
74 | alter table "public"."conversation" add constraint "conversation_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid;
75 |
76 | alter table "public"."conversation" validate constraint "conversation_user_id_fkey";
77 |
78 | alter table "public"."conversation_message" add constraint "conversation_message_conversation_fkey" FOREIGN KEY (conversation) REFERENCES conversation(id) not valid;
79 |
80 | alter table "public"."conversation_message" validate constraint "conversation_message_conversation_fkey";
81 |
82 | alter table "public"."conversation_message" add constraint "conversation_message_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid;
83 |
84 | alter table "public"."conversation_message" validate constraint "conversation_message_user_id_fkey";
85 |
86 | create policy "Enable select for authenticated users only"
87 | on "public"."chat_agent"
88 | as permissive
89 | for select
90 | to authenticated
91 | using (true);
92 |
93 |
94 | create policy "Individuals can view their own conversation."
95 | on "public"."conversation"
96 | as permissive
97 | for select
98 | to public
99 | using ((auth.uid() = user_id));
100 |
101 |
102 | create policy "Users can delete own conversations."
103 | on "public"."conversation"
104 | as permissive
105 | for delete
106 | to public
107 | using ((auth.uid() = user_id));
108 |
109 |
110 | create policy "Users can insert their own conversations."
111 | on "public"."conversation"
112 | as permissive
113 | for insert
114 | to public
115 | with check ((auth.uid() = user_id));
116 |
117 |
118 | create policy "Users can update own conversations."
119 | on "public"."conversation"
120 | as permissive
121 | for update
122 | to public
123 | using ((auth.uid() = user_id));
124 |
125 |
126 | create policy "Individuals can view their own conversation_message."
127 | on "public"."conversation_message"
128 | as permissive
129 | for select
130 | to public
131 | using ((auth.uid() = user_id));
132 |
133 |
134 | create policy "Users can delete own conversation_message."
135 | on "public"."conversation_message"
136 | as permissive
137 | for delete
138 | to public
139 | using ((auth.uid() = user_id));
140 |
141 |
142 | create policy "Users can insert their own conversation_message."
143 | on "public"."conversation_message"
144 | as permissive
145 | for insert
146 | to public
147 | with check ((auth.uid() = user_id));
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/supabase/migrations/20230221183618_remote_commit.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."journal" add column "metadata" text;
2 |
3 |
4 |
--------------------------------------------------------------------------------
/supabase/migrations/20230221183923_remote_commit.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."journal" drop column "metadata";
2 |
3 |
4 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 | import { resolve } from 'path';
4 |
5 | /** @type {import('@sveltejs/kit').Config} */
6 | const config = {
7 | kit: {
8 | adapter: adapter(),
9 | alias: {
10 | $libSkeleton: resolve('./src/lib_skeleton')
11 | }
12 | },
13 | preprocess: [
14 | vitePreprocess({
15 | postcss: true
16 | })
17 | ],
18 | package: {
19 | // strip test files from packaging
20 | files: (filepath) => {
21 | return filepath.indexOf('test') == -1 ? true : false;
22 | }
23 | }
24 | };
25 |
26 | export default config;
27 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: 'class',
4 | content: [
5 | './src/**/*.{html,js,svelte,ts}',
6 | require('path').join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
7 | ],
8 | theme: {
9 | extend: {}
10 | },
11 | plugins: [
12 | require('@tailwindcss/forms'),
13 | require('@tailwindcss/typography'),
14 | require('@tailwindcss/line-clamp'),
15 | ...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()
16 | ]
17 | };
18 |
--------------------------------------------------------------------------------
/tests/test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('index page has expected h1', async ({ page }) => {
4 | await page.goto('/');
5 | expect(await page.textContent('h1')).toBe('Chat with GPT as you Journal');
6 | });
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | test: {
7 | include: ['src/**/*.{test,spec}.{js,ts}']
8 | }
9 | });
10 |
--------------------------------------------------------------------------------