@christianriedl/utils 1.0.129 → 1.0.130

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/iOpenAI.d.ts CHANGED
@@ -91,3 +91,9 @@ export interface IOpenAIServiceWithVectorStore extends IOpenAIServiceWithTools {
91
91
  deleteVectorStore(vectorStoreId: string): Promise<boolean>;
92
92
  getFileId(fileUrl: string): string | undefined;
93
93
  }
94
+ export interface IOpenAIAppConfig {
95
+ officeAITools: string;
96
+ openAIOptions: string;
97
+ openAITools: string;
98
+ openAISpeechModel: string;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianriedl/utils",
3
- "version": "1.0.129",
3
+ "version": "1.0.130",
4
4
  "description": "Interfaces, local storage, service worker, configuration, application state",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,12 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import { inject, ref, toRef, computed, onUnmounted } from 'vue';
3
- import { appStateSymbol, appConfigSymbol, ESize } from '@christianriedl/utils';
3
+ import { appStateSymbol, appConfigSymbol, ESize, EDevice } from '@christianriedl/utils';
4
4
  import { getOpenAISymbol, IOpenAIService, IResponseResult } from '@christianriedl/utils';
5
5
  import Microphone from '@christianriedl/utils/src/components/Microphone.vue';
6
6
  import Camera from '@christianriedl/utils/src/components/Camera.vue';
7
+ import OpenAIConfig from '@christianriedl/utils/src/components/OpenAIConfig.vue';
7
8
 
8
9
  const props = defineProps<{ prompt: string, onlymicrophone?: boolean, tools?: string[] }>();
9
- const emits = defineEmits<{ (e: 'result', result: string): void }>();
10
+ const emits = defineEmits<{
11
+ (e: 'result', result: string): void,
12
+ (e: 'use', tools: string[], persist: boolean): void
13
+ }>();
10
14
 
11
15
  const getOpenAI = inject(getOpenAISymbol)!;
12
16
  const openAI = getOpenAI();
@@ -17,9 +21,11 @@
17
21
  const qcols = computed(() => appState.size.value >= ESize.md ? 10 : 9);
18
22
  const isCamera = ref(false);
19
23
  const isUpload = ref(false);
24
+ const showConfig = ref(false);
20
25
  const files = ref<File[]>([]);
21
26
  const dataUrl = ref("");
22
27
  const camera = ref<InstanceType<typeof Camera>>();
28
+ const htmlText = ref('');
23
29
 
24
30
  onUnmounted(() => {
25
31
  openAI.clearContext();
@@ -52,11 +58,28 @@
52
58
  }
53
59
  if (answer && answer.text) {
54
60
  emits('result', answer.text);
55
- replyLines.value = answer.text.split('\n');
61
+ if (openAI.options.outputFormat == 'html') {
62
+
63
+ const tempDiv = document.createElement('div');
64
+ tempDiv.innerHTML = answer.text;
65
+ const body = tempDiv.querySelector('body');
66
+ if (body) {
67
+ htmlText.value = body.innerHTML;
68
+ } else {
69
+ // Fallback: use all content if <body> is not present
70
+ htmlText.value = tempDiv.innerHTML;
71
+ }
72
+ }
73
+ else {
74
+ replyLines.value = answer.text.split('\n');
75
+ htmlText.value = '';
76
+ }
56
77
  }
57
78
  else {
58
- emits('result', "KEINE KORREKTE ANTWORT");
59
- replyLines.value = ["NO CORRECT ANSWER"];
79
+ htmlText.value = '';
80
+ const text = "KEINE KORREKTE ANTWORT : " + answer.error;
81
+ emits('result', text);
82
+ replyLines.value = [text];
60
83
  }
61
84
  }
62
85
  async function onResult(result: SpeechRecognitionResult) {
@@ -88,49 +111,62 @@
88
111
  reader.readAsDataURL(files.value[0]);
89
112
  }
90
113
  }
114
+ function onUse(tools: string[], persist: boolean) {
115
+ emits('use', tools, persist);
116
+ }
91
117
  </script>
92
118
 
93
119
  <template>
94
120
  <microphone v-if="props.onlymicrophone" lang="de-AT" @result="onResult"></microphone>
95
121
  <v-container v-else fluid class="bg-office">
96
- <v-row dense align="center" class="font-weight-bold">
97
- <v-col :cols="qcols">
98
- <v-text-field name="subject" type="text" v-model="question" density="compact" hide-details></v-text-field>
99
- </v-col>
100
- <v-col :cols="12 - qcols">
101
- <v-btn class="aibutton" @click="onComplete">AI</v-btn>
102
- <microphone lang="de-AT" cls="aibutton" @result="onResult"></microphone>
103
- <v-btn variant="tonal" class="bg-office aibutton" @click="onCamera">
104
- <v-icon size="large" icon="$webcam" color="primary"></v-icon>
105
- </v-btn>
106
- <v-btn variant="tonal" class="bg-office aibutton" @click="isUpload=true;replyLines=[]">
107
- <v-icon size="large" icon="$image" color="primary"></v-icon>
108
- </v-btn>
109
- </v-col>
110
- </v-row>
111
- <v-row v-if="isCamera" dense align="center">
112
- <v-col cols="6">
113
- <camera :resolution="{ width: 640, height: 480 }" ref="camera" autoplay></camera>
114
- </v-col>
115
- <v-col cols="6">
116
- <v-row v-for="(line, index) in replyLines" :key=index dense align="center">
117
- <v-col cols="12">{{line}}</v-col>
118
- </v-row>
119
- </v-col>
120
- </v-row>
121
- <v-row v-if="isUpload" dense align="center">
122
- <v-col cols="6">
123
- <v-file-input show-size clearable multiple hide-details v-model="files" accept="image/*" label="Select File" prepend-icon="" append-icon="" @update:modelValue="onFile">
124
- </v-file-input>
125
- </v-col>
126
- <v-col cols="6">
127
- <img width="100%" :src="dataUrl">
128
- </img>
129
- </v-col>
130
- </v-row>
131
- <v-row v-else v-for="(line, index) in replyLines" :key=index dense align="center">
132
- <v-col cols="12">{{line}}</v-col>
133
- </v-row>
122
+ <v-row dense align="center" class="font-weight-bold">
123
+ <v-col :cols="qcols">
124
+ <v-text-field name="subject" type="text" v-model="question" density="compact" hide-details></v-text-field>
125
+ </v-col>
126
+ <v-col :cols="12 - qcols">
127
+ <v-btn class="aibutton" @click="onComplete">AI</v-btn>
128
+ <microphone lang="de-AT" cls="aibutton" @result="onResult"></microphone>
129
+ <v-btn variant="tonal" class="bg-office aibutton" @click="onCamera">
130
+ <v-icon size="large" icon="$webcam" color="primary"></v-icon>
131
+ </v-btn>
132
+ <v-btn variant="tonal" class="bg-office aibutton" @click="isUpload=true;replyLines=[]">
133
+ <v-icon size="large" icon="$image" color="primary"></v-icon>
134
+ </v-btn>
135
+ <v-btn variant="tonal" class="bg-office aibutton" @click="showConfig=true">
136
+ <v-icon size="large" icon="$settings" color="primary"></v-icon>
137
+ </v-btn>
138
+ </v-col>
139
+ </v-row>
140
+ <v-row v-if="isCamera" dense align="center">
141
+ <v-col cols="6">
142
+ <camera :resolution="{ width: 640, height: 480 }" ref="camera" autoplay></camera>
143
+ </v-col>
144
+ <v-col cols="6">
145
+ <v-row v-for="(line, index) in replyLines" :key=index dense align="center">
146
+ <v-col cols="12">{{line}}</v-col>
147
+ </v-row>
148
+ </v-col>
149
+ </v-row>
150
+ <v-row v-if="isUpload" dense align="center">
151
+ <v-col cols="6">
152
+ <v-file-input show-size clearable multiple hide-details v-model="files" accept="image/*" label="Select File" prepend-icon="" append-icon="" @update:modelValue="onFile">
153
+ </v-file-input>
154
+ </v-col>
155
+ <v-col cols="6">
156
+ <img width="100%" :src="dataUrl">
157
+ </v-col>
158
+ </v-row>
159
+ <v-row v-if="htmlText" dense >
160
+ <v-col cols="12">
161
+ <div v-html="htmlText"></div>
162
+ </v-col>
163
+ </v-row>
164
+ <v-row v-else v-for="(line, index) in replyLines" :key=index dense align="center">
165
+ <v-col cols="12">{{line}}</v-col>
166
+ </v-row>
167
+ <v-dialog v-model="showConfig">
168
+ <OpenAIConfig :tools="props.tools" @use="onUse" @back="showConfig=false"></OpenAIConfig>
169
+ </v-dialog>
134
170
  </v-container>
135
171
  </template>
136
172
 
@@ -0,0 +1,209 @@
1
+ <script setup lang="ts">
2
+ import { inject, ref, reactive } from 'vue';
3
+ import { appConfigSymbol, AppConfig, Dictionary } from '@christianriedl/utils';
4
+ import { getOpenAISymbol, IOpenAIServiceWithVectorStore, IOpenAIAppConfig, IOpenAIOptions, ITool } from '@christianriedl/utils';
5
+
6
+ const props = defineProps<{ tools?: string[] }>();
7
+ const emits = defineEmits<{
8
+ (e: 'use', tools: string[], persist: boolean): void,
9
+ (e: 'back'): void
10
+ }>();
11
+
12
+ const getOpenAI = inject(getOpenAISymbol)!;
13
+ const openAI = getOpenAI() as IOpenAIServiceWithVectorStore;
14
+ const appConfig = inject(appConfigSymbol)! as AppConfig;
15
+ const aiAppConfig = appConfig as unknown as IOpenAIAppConfig;
16
+ const options = reactive<IOpenAIOptions>(Object.assign({}, openAI.options));
17
+ const tools = reactive<Dictionary<any>>(Object.assign({}, openAI.aiTools));
18
+ const use = reactive<Dictionary<boolean>>({});
19
+ const optionsChanged = ref(false);
20
+ const toolsChanged = ref(false);
21
+ const useChanged = ref(false);
22
+ const outputFormats = ['text', 'markdown', 'html'];
23
+ const reasoningEffortLevels = ['minimum', 'low', 'medium', 'high', null];
24
+ const mcpApprovals = ['always', 'never', null];
25
+ const vectorStoreNames: string[] = [];
26
+ const fileSearch = reactive<Dictionary<string[]>>({});
27
+
28
+ initializeTools();
29
+
30
+ function initializeTools() {
31
+ for (const key in openAI.vectorStore) {
32
+ vectorStoreNames.push(key);
33
+ }
34
+ for (const key in tools) {
35
+ const tool = tools[key];
36
+ use[key] = false;
37
+ if (props.tools && props.tools.includes(key)) {
38
+ use[key] = true;
39
+ }
40
+ if (tool.type === 'file_search') {
41
+ const names = [];
42
+ for (let i = 0; i < tool.vector_store_ids.length; i++) {
43
+ const vs = openAI.getVectorStore(tool.vector_store_ids[i]);
44
+ if (vs && props.tools && props.tools.includes(vs.name)) {
45
+ names.push(vs.name);
46
+ }
47
+ }
48
+ fileSearch[key] = names;
49
+ }
50
+ if (tool.type.startsWith("web_search")) {
51
+ tool.user_location = Object.assign({}, tool.user_location);
52
+ }
53
+ }
54
+ }
55
+ function onOptionChange() {
56
+ optionsChanged.value = true;
57
+ }
58
+ function onToolChange() {
59
+ toolsChanged.value = true;
60
+ }
61
+ function onUseChange() {
62
+ useChanged.value = true;
63
+ }
64
+ function onSave() {
65
+ const persist = window.confirm('Do you want to persist the changes?');
66
+ if (toolsChanged.value) {
67
+ for (const key in fileSearch) {
68
+ const vsNames = fileSearch[key];
69
+ for (let i = 0; i < vsNames.length; i++) {
70
+ tools[key].vector_store_ids[i] = openAI.vectorStore![vsNames[i]].id;
71
+ }
72
+ }
73
+ openAI.aiTools = tools as Dictionary<ITool>;
74
+ if (persist) {
75
+ aiAppConfig.openAITools = JSON.stringify(openAI.aiTools);
76
+ appConfig.saveItem(appConfig.items['openAITools']);
77
+ }
78
+ toolsChanged.value = false;
79
+ }
80
+ if (optionsChanged.value) {
81
+ openAI.options = options;
82
+ if (persist) {
83
+ aiAppConfig.openAIOptions = JSON.stringify(options);
84
+ appConfig.saveItem(appConfig.items['openAIOptions']);
85
+ }
86
+ optionsChanged.value = false;
87
+ }
88
+ }
89
+ function onSaveUsage() {
90
+ const persist = window.confirm('Do you want to persist the changes?');
91
+ const useTools: string[] = [];
92
+ for (const key in use) {
93
+ if (use[key]) {
94
+ useTools.push(key);
95
+ }
96
+ }
97
+ emits('use', useTools, persist);
98
+ useChanged.value = false;
99
+ }
100
+ </script>
101
+
102
+ <template>
103
+ <v-container fluid class="bg-office">
104
+ <h2>OpenAI Settings</h2>
105
+ <v-row dense align="center">
106
+ <v-col cols="3">
107
+ <v-text-field v-model="options.model" type="string" label="Model" @change="onOptionChange"></v-text-field>
108
+ </v-col>
109
+ <v-col cols="2">
110
+ <v-select v-model="options.outputFormat" :items="outputFormats" persistent-hint
111
+ label="Format" @update:modelValue="onOptionChange" solo single-line>
112
+ </v-select>
113
+ </v-col>
114
+ <v-col cols="2">
115
+ <v-select v-model="options.reasoning_effort_level" :items="reasoningEffortLevels" persistent-hint
116
+ label="Reasoning" @update:modelValue="onOptionChange" solo single-line>
117
+ </v-select>
118
+ </v-col>
119
+ <v-col cols="2">
120
+ <v-text-field v-model="options.max_output_tokens" type="number" label="Max Token" @change="onOptionChange"></v-text-field>
121
+ </v-col>
122
+ <v-col cols="1">
123
+ <v-text-field v-model="options.temperature" type="number" label="Temperature" @change="onOptionChange"></v-text-field>
124
+ </v-col>
125
+ <v-col cols="1">
126
+ <v-text-field v-model="options.top_p" type="number" label="Top P" @change="onOptionChange"></v-text-field>
127
+ </v-col>
128
+ <v-col cols="1">
129
+ <v-checkbox v-model="options.store" label="Store" @change="onOptionChange"></v-checkbox>
130
+ </v-col>
131
+ </v-row>
132
+ <v-row dense align="center">
133
+ <v-col cols="12">
134
+ <v-text-field v-model="options.instructions" type="string" label="System Message" @change="onOptionChange"></v-text-field>
135
+ </v-col>
136
+ </v-row>
137
+ <template v-for="(tool, key) in tools">
138
+ <v-row v-if="tool.type == 'mcp'" dense align="center">
139
+ <v-col cols="2">
140
+ <v-text-field v-model="tool.server_label" disabled type="string" label="mcp" @change="onToolChange"></v-text-field>
141
+ </v-col>
142
+ <v-col cols="4">
143
+ <v-text-field v-model="tool.server_url" type="string" label="Url" @change="onToolChange"></v-text-field>
144
+ </v-col>
145
+ <v-col cols="3">
146
+ <v-text-field v-model="tool.server_description" type="string" label="Description" @change="onToolChange"></v-text-field>
147
+ </v-col>
148
+ <v-col cols="2">
149
+ <v-select v-model="tool.require_approval" :items="mcpApprovals" persistent-hint
150
+ label="Approval" @update:modelValue="onToolChange" solo single-line>
151
+ </v-select>
152
+ </v-col>
153
+ <v-col cols="1">
154
+ <v-checkbox v-model="use[key]" label="Use" @update:modelValue="onUseChange"></v-checkbox>
155
+ </v-col>
156
+ </v-row>
157
+ <v-row v-if="tool.type == 'file_search'" dense align="center">
158
+ <v-col cols="2">
159
+ <v-text-field :model-value="key" disabled type="string" label="file_search" @change="onToolChange"></v-text-field>
160
+ </v-col>
161
+ <v-col cols="7">
162
+ <v-select v-model="fileSearch[key]" :items="vectorStoreNames" multiple persistent-hint
163
+ label="Vector Stores" @update:modelValue="onToolChange" solo single-line>
164
+ </v-select>
165
+ </v-col>
166
+ <v-col cols="2">
167
+ <v-text-field v-model="tool.max_num_results" type="string" label="Max Results" @change="onToolChange"></v-text-field>
168
+ </v-col>
169
+ <v-col cols="1">
170
+ <v-checkbox v-model="use[key]" label="Use" @update:modelValue="onUseChange"></v-checkbox>
171
+ </v-col>
172
+ </v-row>
173
+ <v-row v-if="tool.type.startsWith('web_search')" dense align="center">
174
+ <v-col cols="2">
175
+ <v-text-field :model-value="key" disabled type="string" label="file_search" @change="onToolChange"></v-text-field>
176
+ </v-col>
177
+ <v-col cols="2">
178
+ <v-text-field v-model="tool.user_location.country" type="string" label="Country" @change="onToolChange"></v-text-field>
179
+ </v-col>
180
+ <v-col cols="3">
181
+ <v-text-field v-model="tool.user_location.city" type="string" label="City" @change="onToolChange"></v-text-field>
182
+ </v-col>
183
+ <v-col cols="4">
184
+ <v-text-field v-model="tool.user_location.region" type="string" label="Region" @change="onToolChange"></v-text-field>
185
+ </v-col>
186
+ <v-col cols="1">
187
+ <v-checkbox v-model="use[key]" label="Use" @update:modelValue="onUseChange"></v-checkbox>
188
+ </v-col>
189
+ </v-row>
190
+ </template>
191
+ <v-row dense align="center">
192
+ <v-col cols="2">
193
+ <v-btn class="bg-office" @click="emits('back')">BACK
194
+ <v-icon icon="$back"></v-icon>
195
+ </v-btn>
196
+ </v-col>
197
+ <v-col cols="2">
198
+ <v-btn :disabled="!optionsChanged && !toolsChanged" class="bg-office" @click="onSave">SAVE
199
+ <v-icon icon="$save"></v-icon>
200
+ </v-btn>
201
+ </v-col>
202
+ <v-col cols="2">
203
+ <v-btn :disabled="!useChanged" class="bg-office" @click="onSaveUsage">SAVE TOOL USE
204
+ <v-icon icon="$save"></v-icon>
205
+ </v-btn>
206
+ </v-col>
207
+ </v-row>
208
+ </v-container>
209
+ </template>