Skip to content

Commit 67a98b8

Browse files
authored
Improvement/OpenAI token after merge (#153)
* Start switch to new openai live API. Implement init_chatbot_session. * Start test chatbot in frontend * Use only text chat for now * Attempt at reducing typing speed answer * Start removing state machine for chatbot, adapt test/session chat look-n-feel * Advance on chatbot UI rework * Advanced * Start text-based directory for response API * Use real-time API again, auth tokens don't work with response API * Add connection status. Use different connection for each conversation. * Save/reload chat history (not tested yet) * Use auth token * Fix errors, chat works * Remove unused things in the frontend * Rework chatHistoryContext * Few fixes * Fix chat refresh * Fix first message saving * Fix connection initialization * Update session context based on history * Add autofocus, disable sending message as long as connection is not ready * Introduced auth token context. Remove idempotency call for openai. * Show back chat history * Removed old unused files * Fix border * Consume and check for user AI credits * Removed idempotent proxy: https outcalls support IPv4 (since August 2025) * Use silent audio track for real-time chat. Fix CSP. * Fix real-time session not initializing due to token getting invalid or not getting queried. * Fix messages not being cleared when creating a new chat. Fix default chat name. * Chatbot improvement in progress * Fix async loading of history * Added some cleaning * Activate voice-to-voice * Use upsert instead of refs to concatenate AI content * Restrict voice AI only to premium users * Allow microphone access in CSP * Introduced hooks for saving history * Prevent saving history if too recent * Fix credits depleted popup. Add voice in welcome ai page. * Fix chat ordering. redirect new chat to /chat * Fix user profile that was using previous session info if any * Introduce new menu when clicking on user link, with new route to subscriptions and logout. Introduced useIntProp that use authenticated actor if any else unauthenticated. Fix free voice AI.
1 parent a039b4e commit 67a98b8

30 files changed

+1004
-612
lines changed

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
- npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
66
- dfx: https://internetcomputer.org/docs/current/developer-docs/getting-started/install/#installing-dfx-via-dfxvm
77
- mops: https://docs.mops.one/quick-start
8-
- cargo: https://doc.rust-lang.org/cargo/
98

109
## Install
1110
```bash
@@ -45,11 +44,8 @@ mops test
4544
- once created, IP shall not be listed on the marketplace yet, but only in the user's owned IP
4645

4746
## TODOs
48-
- Verify the idempotent proxy cycles consomption has been reduced by putting the timer every day
49-
- Inject the chatgpt API key instead of hardcoding it
5047
- Fix Conversions.intPropToMetadata to be able to use it
5148
- Harmonize the error messages returned by the backend canister
5249
- Be able to add a subaccount when creating IPs
5350
- Code review of implementation of ICRC-7/ICRC-37/ICRC-3, ideally have them audited
54-
- Have a proper approve flow (comes with NFID)
5551
- Misc frontend todos, nothing blocking or critical

package-lock.json

Lines changed: 88 additions & 87 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@dfinity/utils": "^2.13.2",
2626
"@napi-rs/lzma": "^1.3.1",
2727
"@napi-rs/lzma-linux-x64-gnu": "^1.3.1",
28-
"@nfid/identitykit": "^1.0.15",
28+
"@nfid/identitykit": "^1.0.16",
2929
"@pdf-lib/fontkit": "^1.1.1",
3030
"@stripe/react-stripe-js": "^5.3.0",
3131
"@stripe/stripe-js": "^8.5.1",

src/backend/ChatBot.mo

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import SubscriptionManager "SubscriptionManager";
1616
module {
1717

1818
let CHAT_INSTRUCTIONS = "You are an assistant designed to help the user answer questions on intellectual property (IP). Your are embedded in the BIPQuantum website, which is a platform that delivers digital certificate that leverages blockchain technology to provide secure and immutable proof of ownership and authenticity for intellectual properties. You will answer technical questions on IP and guide the user through the process of creating a new IP certificate. You won't answer questions that e not related to IP, blockchain, or the BIPQuantum platform.";
19+
let AUTH_KEY_TTL_SECONDS = 60;
1920

2021
type Result<Ok, Err> = Result.Result<Ok, Err>;
2122

@@ -47,7 +48,7 @@ module {
4748
"{ " #
4849
"\"expires_after\": " #
4950
"{ \"anchor\": \"created_at\", " #
50-
"\"seconds\": 60 }, " #
51+
"\"seconds\": " # Nat.toText(AUTH_KEY_TTL_SECONDS) # " }, " #
5152
"\"session\": { " #
5253
"\"type\": \"realtime\", " #
5354
"\"model\": \"gpt-realtime\", " #

src/frontend/components/App.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import CookieBanner, { CookieBannerProvider } from "./common/CookieBanner";
1212
import TopBar from "./layout/TopBar";
1313
import { SearchProvider } from "./common/SearchContext";
1414
import { NotificationProvider } from "./common/NotificationContext";
15+
import { UserProvider } from "./common/UserContext";
1516

1617
import "@nfid/identitykit/react/styles.css"
1718
import { IdentityKitProvider } from "@nfid/identitykit/react"
@@ -127,17 +128,19 @@ function App() {
127128
authType={IdentityKitAuthType.DELEGATION}
128129
>
129130
<ActorsProvider>
130-
<ChatHistoryProvider>
131-
<CookieBannerProvider>
132-
<SearchProvider>
133-
<NotificationProvider>
134-
<FungibleLedgerProvider>
135-
<AppContent />
136-
</FungibleLedgerProvider>
137-
</NotificationProvider>
138-
</SearchProvider>
139-
</CookieBannerProvider>
140-
</ChatHistoryProvider>
131+
<UserProvider>
132+
<ChatHistoryProvider>
133+
<CookieBannerProvider>
134+
<SearchProvider>
135+
<NotificationProvider>
136+
<FungibleLedgerProvider>
137+
<AppContent />
138+
</FungibleLedgerProvider>
139+
</NotificationProvider>
140+
</SearchProvider>
141+
</CookieBannerProvider>
142+
</ChatHistoryProvider>
143+
</UserProvider>
141144
</ActorsProvider>
142145
</IdentityKitProvider>
143146
</BrowserRouter>

src/frontend/components/actors/BackendActor.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const useQueryCall = <T extends BackendMethods>(options: UseQueryCallOptions<T>,
2020
const [data, setData] = useState<ExtractReturn<Backend[T]> | undefined>(undefined);
2121
const [loading, setLoading] = useState(false);
2222
const [error, setError] = useState<any>(null);
23+
const ready = !!actor;
2324

2425
const call = useCallback(
2526
async (callArgs?: ExtractArgs<Backend[T]>): Promise<ExtractReturn<Backend[T]> | undefined> => {
@@ -67,7 +68,7 @@ const useQueryCall = <T extends BackendMethods>(options: UseQueryCallOptions<T>,
6768
}
6869
}, [actor, options.functionName, serializeArgs(options.args)]);
6970

70-
return { data, loading, error, call };
71+
return { data, loading, ready, error, call };
7172
};
7273

7374
interface UseUpdateCallOptions<T extends BackendMethods> {
@@ -79,6 +80,7 @@ interface UseUpdateCallOptions<T extends BackendMethods> {
7980
export const useUpdateCall = <T extends BackendMethods>(options: UseUpdateCallOptions<T>, actor: ActorSubclass<Backend> | undefined) => {
8081
const [loading, setLoading] = useState(false);
8182
const [error, setError] = useState<any>(null);
83+
const ready = !!actor;
8284

8385
const call = useCallback(
8486
async (args: ExtractArgs<Backend[T]> = [] as any): Promise<ExtractReturn<Backend[T]> | undefined> => {
@@ -103,7 +105,7 @@ export const useUpdateCall = <T extends BackendMethods>(options: UseUpdateCallOp
103105
[actor, options.functionName, options.onSuccess, options.onError]
104106
);
105107

106-
return { call, loading, error };
108+
return { call, loading, ready, error };
107109
};
108110

109111
export type { Backend };

src/frontend/components/common/SearchContext.tsx

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,20 @@ const VERSION_KEY = "miniSearchVersion";
3434
export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
3535
children,
3636
}) => {
37-
const { authenticated, unauthenticated } = useActors();
37+
const { unauthenticated } = useActors();
3838

3939
const [documents, setDocuments] = useState<Document[]>(() => {
4040
try {
4141
const version = localStorage.getItem(VERSION_KEY);
42-
//console.log("[SearchContext] Current storage version:", version, "Expected:", STORAGE_VERSION);
4342

4443
if (version !== String(STORAGE_VERSION)) {
45-
//console.log("[SearchContext] Version mismatch - clearing old data and migrating to version", STORAGE_VERSION);
4644
localStorage.removeItem(STORAGE_KEY);
4745
localStorage.setItem(VERSION_KEY, String(STORAGE_VERSION));
4846
return [];
4947
}
5048

5149
const raw = localStorage.getItem(STORAGE_KEY);
5250
const docs = raw ? JSON.parse(raw) : [];
53-
//console.log("[SearchContext] Loaded", docs.length, "documents from localStorage");
5451
return docs;
5552
} catch (err) {
5653
console.error("[SearchContext] Failed to parse cached documents:", err);
@@ -68,8 +65,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
6865
return [];
6966
}
7067

71-
//console.log("[SearchContext] Fetching", ids.length, "intProps:", ids);
72-
7368
const results = await Promise.all(
7469
ids.map(async (id) => {
7570
try {
@@ -81,7 +76,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
8176
description: result.ok.intProp.V1.description,
8277
author: result.ok.author?.[0] ?? "",
8378
};
84-
console.log(`[SearchContext] Fetched intProp ${id}:`, doc.title, "by", doc.author || "(no author)");
8579
return doc;
8680
} else {
8781
console.warn(`[SearchContext] Failed to fetch intProp ${id}: result not ok`);
@@ -94,21 +88,17 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
9488
);
9589

9690
const validResults = results.filter((r): r is Document => r !== null);
97-
//console.log("[SearchContext] Successfully fetched", validResults.length, "out of", ids.length, "intProps");
9891
return validResults;
9992
};
10093

10194
const refreshDocuments = useCallback(async () => {
10295
if (!unauthenticated) {
103-
//console.log("[SearchContext] Cannot refresh - unauthenticated actor not available");
10496
return;
10597
}
10698
if (isRefreshing) {
107-
//console.log("[SearchContext] Already refreshing, skipping");
10899
return;
109100
}
110101

111-
//console.log("[SearchContext] Starting refresh of intProp IDs");
112102
setIsRefreshing(true);
113103
try {
114104
const result = await unauthenticated.backend.get_listed_int_props({
@@ -119,7 +109,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
119109

120110
// Ensure result is valid and convert bigints to numbers safely
121111
if (Array.isArray(result)) {
122-
//console.log("[SearchContext] Received", result.length, "intProp IDs from backend");
123112
setIntPropIds(result);
124113
setLastRefreshTime(Date.now());
125114
} else {
@@ -153,13 +142,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
153142
);
154143

155144
if (missingIds.length > 0 || removedIds.length > 0) {
156-
//console.log("[SearchContext] Document sync needed - Missing:", missingIds.length, "Removed:", removedIds.length);
157-
if (missingIds.length > 0) {
158-
//console.log("[SearchContext] Missing IDs:", missingIds);
159-
}
160-
if (removedIds.length > 0) {
161-
//console.log("[SearchContext] Removed IDs:", removedIds);
162-
}
163145

164146
(async () => {
165147
const newDocs = await fetchIntProps(missingIds);
@@ -179,19 +161,15 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
179161
});
180162

181163
const finalDocs = Array.from(docMap.values());
182-
//console.log("[SearchContext] Documents updated - Total:", finalDocs.length);
183164
return finalDocs;
184165
});
185166
})();
186-
} else {
187-
//console.log("[SearchContext] Documents in sync - no updates needed");
188167
}
189168
}, [intPropIds]);
190169

191170
useEffect(() => {
192171
try {
193172
localStorage.setItem(STORAGE_KEY, JSON.stringify(documents));
194-
//console.log("[SearchContext] Saved", documents.length, "documents to localStorage");
195173
} catch (err) {
196174
console.error("[SearchContext] Failed to cache documents to localStorage:", err);
197175
}
@@ -204,7 +182,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
204182
};
205183

206184
const miniSearch = useMemo(() => {
207-
//console.log("[SearchContext] Rebuilding MiniSearch index with", documents.length, "documents");
208185
const search = new MiniSearch(options);
209186

210187
try {
@@ -220,9 +197,6 @@ export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
220197

221198
if (uniqueDocuments.length > 0) {
222199
search.addAll(uniqueDocuments);
223-
//console.log("[SearchContext] MiniSearch index built successfully with", uniqueDocuments.length, "documents");
224-
} else {
225-
//console.log("[SearchContext] MiniSearch index is empty - no documents to index");
226200
}
227201
} catch (error) {
228202
console.error("[SearchContext] Error adding documents to MiniSearch:", error);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { createContext, useContext, ReactNode, useEffect, useState } from "react";
2+
import { useAuth } from "@nfid/identitykit/react";
3+
import { backendActor } from "../actors/BackendActor";
4+
import { User } from "../../../declarations/backend/backend.did";
5+
import { fromNullable } from "@dfinity/utils";
6+
7+
interface UserContextType {
8+
user: User | null;
9+
isLoading: boolean;
10+
refetchUser: () => void;
11+
}
12+
13+
const UserContext = createContext<UserContextType | undefined>(undefined);
14+
15+
export const useUser = () => {
16+
const context = useContext(UserContext);
17+
if (!context) {
18+
throw new Error("useUser must be used within UserProvider");
19+
}
20+
return context;
21+
};
22+
23+
interface UserProviderProps {
24+
children: ReactNode;
25+
}
26+
27+
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
28+
const { user: authUser } = useAuth();
29+
const [user, setUser] = useState<User | null>(null);
30+
const [isLoading, setIsLoading] = useState(false);
31+
32+
const { data: queriedUser, call: fetchUser } = backendActor.unauthenticated.useQueryCall({
33+
functionName: "get_user",
34+
args: authUser ? [authUser.principal] : undefined,
35+
});
36+
37+
// Fetch user when authentication state changes
38+
useEffect(() => {
39+
if (authUser) {
40+
setIsLoading(true);
41+
fetchUser([authUser.principal]);
42+
} else {
43+
// Not authenticated - use default user (null)
44+
setUser(null);
45+
setIsLoading(false);
46+
}
47+
}, [authUser?.principal.toText()]);
48+
49+
// Update user state when query completes
50+
useEffect(() => {
51+
if (queriedUser !== undefined) {
52+
const userData = fromNullable(queriedUser);
53+
// If get_user returns null, use default user (null)
54+
setUser(userData || null);
55+
} else {
56+
setUser(null);
57+
}
58+
setIsLoading(false);
59+
}, [queriedUser]);
60+
61+
const refetchUser = () => {
62+
if (authUser) {
63+
setIsLoading(true);
64+
fetchUser([authUser.principal]);
65+
}
66+
};
67+
68+
return (
69+
<UserContext.Provider value={{ user, isLoading, refetchUser }}>
70+
{children}
71+
</UserContext.Provider>
72+
);
73+
};

src/frontend/components/common/UserDetails.tsx

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)