From e4c3d7afb1f784eb43f66ee443b9bfd721680ad2 Mon Sep 17 00:00:00 2001 From: Vitali Semianiaka Date: Wed, 11 Feb 2026 02:12:03 +0300 Subject: [PATCH] update quality --- MyCompany.MyProject.BackendApi/Program.cs | 40 +- .../components/playground/CallHistoryCard.vue | 138 ++++ .../playground/ComponentInfoCard.vue | 92 +++ .../playground/ComponentSelectionSidebar.vue | 96 +++ .../playground/MainComponentArea.vue | 125 ++++ .../playground/PlaygroundToolbar.vue | 48 ++ QuasarFrontend/src/components/s8n-runtime.ts | 12 +- .../components_s8n/ComponentCalculator.vue | 4 +- .../src/composables/useCallHistory.ts | 154 +++++ .../src/composables/useComponentStore.ts | 139 ++++ QuasarFrontend/src/composables/useDarkMode.ts | 34 + QuasarFrontend/src/layouts/MainLayout.vue | 61 +- QuasarFrontend/src/layouts/layoutVariables.ts | 3 + QuasarFrontend/src/pages/PlaygroundPage.vue | 633 ++++-------------- .../src/services/callHistoryStorage.ts | 2 +- S8n.Components.Packages/Basics/Calculator.cs | 10 + S8n.Components.Packages/Basics/HttpRequest.cs | 7 +- components | 2 +- 18 files changed, 1036 insertions(+), 564 deletions(-) create mode 100644 QuasarFrontend/src/components/playground/CallHistoryCard.vue create mode 100644 QuasarFrontend/src/components/playground/ComponentInfoCard.vue create mode 100644 QuasarFrontend/src/components/playground/ComponentSelectionSidebar.vue create mode 100644 QuasarFrontend/src/components/playground/MainComponentArea.vue create mode 100644 QuasarFrontend/src/components/playground/PlaygroundToolbar.vue create mode 100644 QuasarFrontend/src/composables/useCallHistory.ts create mode 100644 QuasarFrontend/src/composables/useComponentStore.ts create mode 100644 QuasarFrontend/src/composables/useDarkMode.ts create mode 100644 QuasarFrontend/src/layouts/layoutVariables.ts diff --git a/MyCompany.MyProject.BackendApi/Program.cs b/MyCompany.MyProject.BackendApi/Program.cs index 268e3ba..ecd3275 100644 --- a/MyCompany.MyProject.BackendApi/Program.cs +++ b/MyCompany.MyProject.BackendApi/Program.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text.Json; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Scalar.AspNetCore; // Ensure S8n.Components.Packages assembly is loaded @@ -85,7 +86,7 @@ app.MapGet("/api/components", () => .WithName("GetComponents"); // Runtime execution endpoint -app.MapPost("/api/runtime/execute", (RuntimeRequest request) => +app.MapPost("/api/runtime/execute", async (RuntimeRequest request) => { try { @@ -166,6 +167,43 @@ app.MapPost("/api/runtime/execute", (RuntimeRequest request) => result = method.Invoke(instance, null); } + Console.Error.WriteLine($"Invocation result type: {result?.GetType()}"); + // If method returns a Task, await it + if (result is Task taskResult) + { + Console.Error.WriteLine($"Detected Task, awaiting..."); + await taskResult.ConfigureAwait(false); + Console.Error.WriteLine($"Task completed, status: {taskResult.Status}"); + + // If it's a Task, get the result via reflection + var resultType = taskResult.GetType(); + Console.Error.WriteLine($"Task result type: {resultType}"); + if (typeof(Task).IsAssignableFrom(resultType)) + { + Console.Error.WriteLine($"Generic Task detected"); + // Get the Result property + var resultProperty = resultType.GetProperty("Result"); + if (resultProperty != null) + { + result = resultProperty.GetValue(taskResult); + Console.Error.WriteLine($"Result value: {result}"); + Console.Error.WriteLine($"Result value type: {result?.GetType()}"); + } + else + { + Console.Error.WriteLine($"Result property not found"); + result = null; + } + } + else + { + // Task (non-generic) returns nothing + Console.Error.WriteLine($"Non-generic Task, setting result to null"); + result = null; + } + } + + Console.Error.WriteLine($"Final result: {result}"); return Results.Ok(new RuntimeResponse { Outputs = result diff --git a/QuasarFrontend/src/components/playground/CallHistoryCard.vue b/QuasarFrontend/src/components/playground/CallHistoryCard.vue new file mode 100644 index 0000000..3407385 --- /dev/null +++ b/QuasarFrontend/src/components/playground/CallHistoryCard.vue @@ -0,0 +1,138 @@ + + + diff --git a/QuasarFrontend/src/components/playground/ComponentInfoCard.vue b/QuasarFrontend/src/components/playground/ComponentInfoCard.vue new file mode 100644 index 0000000..a52c359 --- /dev/null +++ b/QuasarFrontend/src/components/playground/ComponentInfoCard.vue @@ -0,0 +1,92 @@ + + + diff --git a/QuasarFrontend/src/components/playground/ComponentSelectionSidebar.vue b/QuasarFrontend/src/components/playground/ComponentSelectionSidebar.vue new file mode 100644 index 0000000..e2ebd98 --- /dev/null +++ b/QuasarFrontend/src/components/playground/ComponentSelectionSidebar.vue @@ -0,0 +1,96 @@ + + + diff --git a/QuasarFrontend/src/components/playground/MainComponentArea.vue b/QuasarFrontend/src/components/playground/MainComponentArea.vue new file mode 100644 index 0000000..b6f5bae --- /dev/null +++ b/QuasarFrontend/src/components/playground/MainComponentArea.vue @@ -0,0 +1,125 @@ + + + diff --git a/QuasarFrontend/src/components/playground/PlaygroundToolbar.vue b/QuasarFrontend/src/components/playground/PlaygroundToolbar.vue new file mode 100644 index 0000000..e078dc4 --- /dev/null +++ b/QuasarFrontend/src/components/playground/PlaygroundToolbar.vue @@ -0,0 +1,48 @@ + + + diff --git a/QuasarFrontend/src/components/s8n-runtime.ts b/QuasarFrontend/src/components/s8n-runtime.ts index baabbd1..be51bb6 100644 --- a/QuasarFrontend/src/components/s8n-runtime.ts +++ b/QuasarFrontend/src/components/s8n-runtime.ts @@ -45,9 +45,8 @@ export const runtime = { }), }); - let response: Response | undefined; - await request.then(r => { + await request.then((r) => { response = r; statusCode.value = response.status; }); @@ -63,13 +62,16 @@ export const runtime = { if (data.error) { throw new Error(data.error); } - - outputs.value = data.outputs || {}; if (data.error) { error.value = data.error; } - return outputs.value; + if (method === 'Execute') { + outputs.value = data.outputs || {}; + return outputs.value; + } else { + return data.outputs || {}; + } } catch (err) { console.error('Runtime execution error:', err); error.value = err instanceof Error ? err.message : 'An error occurred'; diff --git a/QuasarFrontend/src/components_s8n/ComponentCalculator.vue b/QuasarFrontend/src/components_s8n/ComponentCalculator.vue index 48bd6f9..84ff2ff 100644 --- a/QuasarFrontend/src/components_s8n/ComponentCalculator.vue +++ b/QuasarFrontend/src/components_s8n/ComponentCalculator.vue @@ -41,7 +41,7 @@ {{ error }} -
+
Result: {{ outputs.result }} ({{ duration }}ms)
@@ -83,7 +83,7 @@ defineExpose({ width: 100%; .result-section { - min-height: 60px; + min-height: 3em; display: flex; align-items: center; justify-content: center; diff --git a/QuasarFrontend/src/composables/useCallHistory.ts b/QuasarFrontend/src/composables/useCallHistory.ts new file mode 100644 index 0000000..b0f3843 --- /dev/null +++ b/QuasarFrontend/src/composables/useCallHistory.ts @@ -0,0 +1,154 @@ +import { ref, computed, watch } from 'vue' + +export interface CallHistoryItem { + id: string + componentCode: string + componentName: string + timestamp: number + inputs: unknown + error?: string + duration: number + statusCode?: number +} + +const HISTORY_STORAGE_KEY = 's8n_call_history' +const MAX_HISTORY_ITEMS = 50 +const KEEP_REQUEST_INFO_KEY = 's8n_keep_request_info' + +export function useCallHistory(currentComponentCode?: () => string | undefined) { + const callHistory = ref([]) + const keepRequestInfo = ref(false) + const callHistoryFilter = ref<'all' | 'current'>('all') + + const loadKeepRequestInfo = () => { + const stored = localStorage.getItem(KEEP_REQUEST_INFO_KEY) + if (stored) { + try { + keepRequestInfo.value = JSON.parse(stored) + } catch (e) { + console.error('Failed to parse keep request info', e) + } + } + } + + const saveKeepRequestInfo = () => { + localStorage.setItem(KEEP_REQUEST_INFO_KEY, JSON.stringify(keepRequestInfo.value)) + } + + const loadCallHistory = () => { + const stored = localStorage.getItem(HISTORY_STORAGE_KEY) + if (stored) { + try { + callHistory.value = JSON.parse(stored) + } catch (e) { + console.error('Failed to parse call history', e) + } + } + } + + const saveCallHistory = () => { + localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(callHistory.value)) + } + + const addCallHistory = (item: Omit) => { + // Deep clone inputs to prevent reference sharing + const clonedItem = { + ...item, + inputs: item.inputs !== undefined ? JSON.parse(JSON.stringify(item.inputs)) : item.inputs, + } + + // If keepRequestInfo is false, strip sensitive or large input data + const processedItem = { ...clonedItem } + if (!keepRequestInfo.value) { + // Keep only essential metadata about inputs, not the full data + if (processedItem.inputs && typeof processedItem.inputs === 'object') { + const inputs = processedItem.inputs as Record + // Create a minimal representation showing input structure without values + const minimalInputs: Record = {} + for (const key in inputs) { + if (Object.prototype.hasOwnProperty.call(inputs, key)) { + const value = inputs[key] + if (value === null || value === undefined) { + minimalInputs[key] = value + } else if (Array.isArray(value)) { + minimalInputs[key] = `[Array of ${value.length} items]` + } else if (typeof value === 'object') { + minimalInputs[key] = `{Object with ${Object.keys(value).length} keys}` + } else if (typeof value === 'string' && value.length > 50) { + minimalInputs[key] = `${value.substring(0, 47)}...` + } else { + minimalInputs[key] = value + } + } + } + processedItem.inputs = minimalInputs + } + } + + const newItem = { + ...processedItem, + id: Date.now().toString() + Math.random().toString(36).substring(2), + } + callHistory.value.unshift(newItem) + if (callHistory.value.length > MAX_HISTORY_ITEMS) { + callHistory.value = callHistory.value.slice(0, MAX_HISTORY_ITEMS) + } + saveCallHistory() + } + + const clearCallHistory = () => { + callHistory.value = [] + saveCallHistory() + } + + const getStatusColor = (statusCode: number): string => { + if (statusCode >= 200 && statusCode < 300) return 'positive' + if (statusCode >= 300 && statusCode < 400) return 'info' + if (statusCode >= 400 && statusCode < 500) return 'warning' + if (statusCode >= 500) return 'negative' + return 'grey' + } + + const truncateError = (error: string): string => { + if (error.length <= 50) return error + return error.substring(0, 47) + '...' + } + + const selectHistoryItem = (item: CallHistoryItem) => { + // For now, just log the selection + console.log('Selected history item:', item) + // Could implement functionality to load the component and inputs + } + + const filteredCallHistory = computed(() => { + const history = callHistory.value + if (callHistoryFilter.value === 'all') { + return history + } + // filter by current component + const currentCode = currentComponentCode ? currentComponentCode() : undefined + if (!currentCode) { + return history + } + return history.filter((item) => item.componentCode === currentCode) + }) + + // Watch keepRequestInfo to save changes + watch(keepRequestInfo, () => { + saveKeepRequestInfo() + }) + + return { + callHistory, + keepRequestInfo, + callHistoryFilter, + loadKeepRequestInfo, + loadCallHistory, + addCallHistory, + clearCallHistory, + getStatusColor, + truncateError, + selectHistoryItem, + filteredCallHistory, + } +} diff --git a/QuasarFrontend/src/composables/useComponentStore.ts b/QuasarFrontend/src/composables/useComponentStore.ts new file mode 100644 index 0000000..b3fadea --- /dev/null +++ b/QuasarFrontend/src/composables/useComponentStore.ts @@ -0,0 +1,139 @@ +import { ref, computed } from 'vue' +import { defineAsyncComponent, type AsyncComponentLoader } from 'vue' + +export interface ComponentInput { + name: string + type: string + description: string + enum?: string[] + required: boolean +} + +export interface ComponentOutput { + name: string + type: string + description: string +} + +export interface ApiComponent { + code: string + name: string + description: string + icon: string + class: string + methods: string[] + gui: string + category: string + tags: string[] + inputs: ComponentInput[] + outputs: ComponentOutput[] + source: string +} + +export interface ComponentInfo { + code: string + name: string + description: string + icon: string + className: string + method: string + methods: string[] + tags: string[] + component: ReturnType + inputs: ComponentInput[] + outputs: ComponentOutput[] + source: string + category: string +} + +export function useComponentStore() { + const selectedComponent = ref('') + const availableComponents = ref([]) + const loading = ref(false) + const error = ref(null) + const searchQuery = ref('') + + // Map API component to ComponentInfo + const mapApiComponent = (apiComponent: ApiComponent): ComponentInfo => { + const shortCode = apiComponent.code.split('.').pop() || apiComponent.code + + let componentImport: AsyncComponentLoader + switch (apiComponent.gui) { + case 'ComponentCalculator.vue': + componentImport = () => import('../components_s8n/ComponentCalculator.vue') + break + case 'ComponentHttpRequest.vue': + componentImport = () => import('../components_s8n/ComponentHttpRequest.vue') + break + default: + componentImport = () => import('../components_s8n/ComponentCalculator.vue') + } + + return { + code: shortCode, + name: apiComponent.name, + description: apiComponent.description, + icon: apiComponent.icon, + className: apiComponent.class, + method: apiComponent.methods[0] || 'Execute', + methods: apiComponent.methods, + tags: apiComponent.tags || [], + component: defineAsyncComponent(componentImport), + inputs: apiComponent.inputs, + outputs: apiComponent.outputs, + source: apiComponent.source, + category: apiComponent.category, + } + } + + // Fetch components from backend + const fetchComponents = async () => { + loading.value = true + error.value = null + try { + const response = await fetch('/api/components') + if (!response.ok) { + throw new Error(`Failed to fetch components: ${response.statusText}`) + } + const data: ApiComponent[] = await response.json() + availableComponents.value = data.map(mapApiComponent) + + // Auto-select first component if none selected + if (availableComponents.value.length > 0 && !selectedComponent.value) { + selectedComponent.value = availableComponents.value[0]!.code + } + } catch (err) { + error.value = err instanceof Error ? err.message : 'Unknown error' + console.error('Failed to load components:', err) + } finally { + loading.value = false + } + } + + const currentComponentInfo = computed(() => { + return availableComponents.value.find((c) => c.code === selectedComponent.value) + }) + + const filteredComponents = computed(() => { + const query = searchQuery.value.trim().toLowerCase() + if (!query) return availableComponents.value + + return availableComponents.value.filter((comp) => { + const nameMatch = comp.name.toLowerCase().includes(query) + const descMatch = comp.description.toLowerCase().includes(query) + const tagsMatch = comp.tags.some((tag) => tag.toLowerCase().includes(query)) + return nameMatch || descMatch || tagsMatch + }) + }) + + return { + selectedComponent, + availableComponents, + loading, + error, + searchQuery, + fetchComponents, + currentComponentInfo, + filteredComponents, + } +} diff --git a/QuasarFrontend/src/composables/useDarkMode.ts b/QuasarFrontend/src/composables/useDarkMode.ts new file mode 100644 index 0000000..97de456 --- /dev/null +++ b/QuasarFrontend/src/composables/useDarkMode.ts @@ -0,0 +1,34 @@ +import { useQuasar } from 'quasar' + +const DARK_MODE_STORAGE_KEY = 's8n_dark_mode' + +export function useDarkMode() { + const $q = useQuasar() + + const toggleDarkMode = () => { + const newDarkMode = !$q.dark.isActive + $q.dark.set(newDarkMode) + saveDarkModePreference(newDarkMode) + } + + const loadDarkModePreference = () => { + const stored = localStorage.getItem(DARK_MODE_STORAGE_KEY) + if (stored !== null) { + const isDark = JSON.parse(stored) + $q.dark.set(isDark) + } else { + // Default to system preference if no stored preference + $q.dark.set('auto') + } + } + + const saveDarkModePreference = (isDark: boolean) => { + localStorage.setItem(DARK_MODE_STORAGE_KEY, JSON.stringify(isDark)) + } + + return { + toggleDarkMode, + loadDarkModePreference, + isDark: $q.dark.isActive, + } +} diff --git a/QuasarFrontend/src/layouts/MainLayout.vue b/QuasarFrontend/src/layouts/MainLayout.vue index 965639e..0066d50 100644 --- a/QuasarFrontend/src/layouts/MainLayout.vue +++ b/QuasarFrontend/src/layouts/MainLayout.vue @@ -1,28 +1,9 @@ diff --git a/QuasarFrontend/src/layouts/layoutVariables.ts b/QuasarFrontend/src/layouts/layoutVariables.ts new file mode 100644 index 0000000..6b915d6 --- /dev/null +++ b/QuasarFrontend/src/layouts/layoutVariables.ts @@ -0,0 +1,3 @@ +import { ref } from 'vue'; + +export const leftMenuOpened = ref(true); diff --git a/QuasarFrontend/src/pages/PlaygroundPage.vue b/QuasarFrontend/src/pages/PlaygroundPage.vue index 171ee33..85246df 100644 --- a/QuasarFrontend/src/pages/PlaygroundPage.vue +++ b/QuasarFrontend/src/pages/PlaygroundPage.vue @@ -1,427 +1,111 @@