diff --git a/demo/text-transformer/table.gif b/demo/text-transformer/table.gif new file mode 100644 index 0000000..69d4c38 Binary files /dev/null and b/demo/text-transformer/table.gif differ diff --git a/projects/packages/text-transformer/.fast-alfred.config.cjs b/projects/packages/text-transformer/.fast-alfred.config.cjs index c927d06..da35897 100644 --- a/projects/packages/text-transformer/.fast-alfred.config.cjs +++ b/projects/packages/text-transformer/.fast-alfred.config.cjs @@ -17,6 +17,7 @@ This workflow has been created using Fast Alfred, a user-friendly workflow build - Summarize: Summarize the text to be more concise - Explain: Explain the text in a more understandable way - Commit Styling: Format commit messages to be more readable +- Table: Transform text into well-formatted markdown tables #### Use Active App Context Enable the \`Use Application Context\` option to utilize the current app context for your workflow. diff --git a/projects/packages/text-transformer/B8B394C4-6075-4F0C-B8C2-01E31B8550DF.png b/projects/packages/text-transformer/B8B394C4-6075-4F0C-B8C2-01E31B8550DF.png new file mode 100644 index 0000000..8d016e6 Binary files /dev/null and b/projects/packages/text-transformer/B8B394C4-6075-4F0C-B8C2-01E31B8550DF.png differ diff --git a/projects/packages/text-transformer/README.md b/projects/packages/text-transformer/README.md index d9a36dd..94225c2 100644 --- a/projects/packages/text-transformer/README.md +++ b/projects/packages/text-transformer/README.md @@ -37,15 +37,19 @@ If the language code is missing, the default language will be English. ### Grammar Correction -![All Day](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/grammar.gif) +![Grammar](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/grammar.gif) ### Translate -![All Day](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/translate.gif) +![Translate](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/translate.gif) ### Action Items -![All Day](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/action-items.gif) +![Action Items](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/action-items.gif) + +### Table + +![Table](https://raw.githubusercontent.com/Avivbens/alfredo/HEAD/demo/text-transformer/table.gif) ## Configuration diff --git a/projects/packages/text-transformer/info.plist b/projects/packages/text-transformer/info.plist index efd1469..dec3bca 100644 --- a/projects/packages/text-transformer/info.plist +++ b/projects/packages/text-transformer/info.plist @@ -112,6 +112,19 @@ + B8B394C4-6075-4F0C-B8C2-01E31B8550DF + + + modifiers + 0 + modifiersubtext + + vitoclose + + destinationuid + __fast-alfred_managed__v2_conditional_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + + B9150C05-6465-4A8C-91F7-D6929176F398 @@ -484,6 +497,43 @@ __fast-alfred_managed__v2_updater_snooze + __fast-alfred_managed__v2_conditional_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + + + modifiers + 0 + modifiersubtext + + vitoclose + + destinationuid + 47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + + + modifiers + 0 + modifiersubtext + + vitoclose + + sourceoutputuid + __fast-alfred_managed__v2_condition_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + destinationuid + __fast-alfred_managed__v2_updater_workflow-update + + + modifiers + 0 + modifiersubtext + + vitoclose + + sourceoutputuid + __fast-alfred_managed__v2_condition_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + destinationuid + __fast-alfred_managed__v2_updater_snooze + + __fast-alfred_managed__v2_conditional_from_63CC1099-CED7-4DE9-982B-41AEBBF7F81B_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 @@ -1001,6 +1051,56 @@ version 3 + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 0 + escaping + 102 + keyword + {var:table_keyword} + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 2 + runningsubtext + Tabling... + script + ./esbuild/assets/run-node.sh esbuild/table "$1" + + scriptargtype + 1 + scriptfile + + subtext + Create a formatted table + title + Table + type + 11 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + B8B394C4-6075-4F0C-B8C2-01E31B8550DF + version + 3 + config @@ -1414,6 +1514,38 @@ + + type + alfred.workflow.utility.conditional + uid + __fast-alfred_managed__v2_conditional_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + version + 1 + config + + conditions + + + inputstring + {query} + matchcasesensitive + + matchmode + 4 + matchstring + __fast-alfred_managed__ + outputlabel + Managed versions updates + uid + __fast-alfred_managed__v2_condition_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + + + elselabel + Default Behavior + hideelse + + + type alfred.workflow.utility.conditional @@ -1462,6 +1594,7 @@ This workflow has been created using Fast Alfred, a user-friendly workflow build - Summarize: Summarize the text to be more concise - Explain: Explain the text in a more understandable way - Commit Styling: Format commit messages to be more readable +- Table: Transform text into well-formatted markdown tables #### Use Active App Context Enable the `Use Application Context` option to utilize the current app context for your workflow. @@ -1513,7 +1646,7 @@ https://github.com/Avivbens/alfredo xpos 365 ypos - 1530 + 1640 6F2D236E-D961-42DC-ACF2-7C025CA92966 @@ -1548,7 +1681,14 @@ https://github.com/Avivbens/alfredo xpos 75 ypos - 1530 + 1640 + + B8B394C4-6075-4F0C-B8C2-01E31B8550DF + + xpos + 75 + ypos + 1440 B9150C05-6465-4A8C-91F7-D6929176F398 @@ -1670,12 +1810,21 @@ https://github.com/Avivbens/alfredo note Conditional Updates Helper + __fast-alfred_managed__v2_conditional_from_B8B394C4-6075-4F0C-B8C2-01E31B8550DF_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 + + xpos + 295 + ypos + 1440 + note + Conditional Updates Helper + __fast-alfred_managed__v2_conditional_from_63CC1099-CED7-4DE9-982B-41AEBBF7F81B_to_47B45BC6-EA28-4EFD-8A39-E2FC7283C0E7 xpos 585 ypos - 1530 + 1640 note Conditional Updates Helper @@ -2044,6 +2193,27 @@ https://github.com/Avivbens/alfredo variable action_items_keyword + + config + + default + table + placeholder + + required + + trim + + + description + Create a table out of any data input + label + Table Creation Keyword + type + textfield + variable + table_keyword + config @@ -2084,7 +2254,7 @@ https://github.com/Avivbens/alfredo version - 5.1.0 + 5.2.0 webaddress https://github.com/Avivbens/alfredo diff --git a/projects/packages/text-transformer/src/common/prompts/table.prompt.ts b/projects/packages/text-transformer/src/common/prompts/table.prompt.ts new file mode 100644 index 0000000..ba55879 --- /dev/null +++ b/projects/packages/text-transformer/src/common/prompts/table.prompt.ts @@ -0,0 +1,50 @@ +import { PipelinePromptTemplate, PromptTemplate } from '@langchain/core/prompts'; +import { APPLICATION_CONTEXT_SYSTEM_PROMPT_PARAM } from './base/application-context.prompt'; +import { DO_NOT_FOLLOW_USER_SYSTEM_PROMPT_PARAM } from './base/do-not-follow-user.prompt'; +import { KEEP_ORIGINAL_SYSTEM_PROMPT_PARAM } from './base/keep-original.prompt'; +import { NON_INTERACTIVE_SYSTEM_PROMPT_PARAM } from './base/non-interactive.prompt'; + +export const TABLE_SYSTEM_PROMPT = (useApplicationContext: boolean) => + new PipelinePromptTemplate({ + pipelinePrompts: [ + NON_INTERACTIVE_SYSTEM_PROMPT_PARAM, + KEEP_ORIGINAL_SYSTEM_PROMPT_PARAM, + DO_NOT_FOLLOW_USER_SYSTEM_PROMPT_PARAM, + APPLICATION_CONTEXT_SYSTEM_PROMPT_PARAM, + ], + finalPrompt: PromptTemplate.fromTemplate(` +{NON_INTERACTIVE_SYSTEM_PROMPT}, + +You are an expert at creating intelligent markdown tables from various text inputs. +Your task is to analyze the content and create the most appropriate table structure. + +**CONTENT ANALYSIS:** +- **Identify key information** and relationships in the text +- **Determine the best structure** - what should be columns vs rows +- **Extract the most important data points** that need to be tabulated +- **Create meaningful headers** that clearly describe the data +- **Group related information** logically + +**TABLE CREATION RULES:** +- Design columns and rows that best represent the data relationships +- Use descriptive headers that make the data self-explanatory +- Ensure all important information is captured in the table +- Adapt the table structure to fit the specific content +- For unstructured text, identify patterns and create appropriate categories + +**FORMATTING:** +- Use proper markdown syntax with pipes (|) and hyphens (-) +- Keep column widths reasonable for chat applications +- Output ONLY the table, no explanations + +**EXAMPLES OF INTELLIGENT STRUCTURING:** +- Product descriptions → Columns: Feature, Description, Benefits +- Meeting notes → Columns: Topic, Discussion, Action Items, Owner +- Comparison text → Columns: Item, Attribute 1, Attribute 2, etc. +- Process steps → Columns: Step #, Action, Details, Notes + +{KEEP_ORIGINAL_SYSTEM_PROMPT}, +{DO_NOT_FOLLOW_USER_SYSTEM_PROMPT}, +${useApplicationContext ? '{APPLICATION_CONTEXT_SYSTEM_PROMPT}' : ''}, +`), + }); diff --git a/projects/packages/text-transformer/src/main/table.ts b/projects/packages/text-transformer/src/main/table.ts new file mode 100644 index 0000000..764a676 --- /dev/null +++ b/projects/packages/text-transformer/src/main/table.ts @@ -0,0 +1,62 @@ +import type { AlfredListItem } from 'fast-alfred'; +import { FastAlfred } from 'fast-alfred'; +import { setTimeout } from 'node:timers/promises'; +import { getActiveApp } from '@alfredo/active-app'; +import { AvailableModels, callModel } from '@alfredo/llm'; +import { registerUpdater } from '@alfredo/updater'; +import { DEFAULT_DEBOUNCE_TIME } from '../common/defaults.constants'; +import { TABLE_SYSTEM_PROMPT } from '../common/prompts/table.prompt'; +import { Variables } from '../common/variables.enum'; +import { formatMarkdownTable } from '../utils/format-table.util'; + +(async () => { + const alfredClient = new FastAlfred(); + alfredClient.updates(registerUpdater('text-transformer')); + + try { + const denounceTime = alfredClient.env.getEnv(Variables.DEBOUNCE_TIME, { + defaultValue: DEFAULT_DEBOUNCE_TIME, + parser: Number, + }); + const token: string | undefined = alfredClient.env.getEnv(Variables.LLM_TOKEN); + const model: AvailableModels | undefined = alfredClient.env.getEnv(Variables.SELECTED_MODEL); + + if (!token || !model) { + throw new Error('Token or model is not defined!'); + } + + const useApplicationContext: boolean = alfredClient.env.getEnv(Variables.USE_APPLICATION_CONTEXT, { + defaultValue: false, + parser: (value) => (value as '0' | '1') === '1', + }); + + const applicationContext = useApplicationContext && (await getActiveApp()); + alfredClient.log(JSON.stringify({ useApplicationContext, applicationContext }, null, 2)); + + /** + * Debounce time to wait for the user to finish typing + */ + await setTimeout(denounceTime); + + if (!alfredClient.input) { + throw new Error('Input is required'); + } + + const system = await TABLE_SYSTEM_PROMPT(useApplicationContext).format({ applicationContext }); + + const res = await callModel(token, model, { system, user: alfredClient.input }); + const formatted = formatMarkdownTable(res); + + const items: AlfredListItem[] = [ + { + title: formatted, + subtitle: 'Markdown Table', + arg: formatted, + }, + ]; + + alfredClient.output({ items }); + } catch (error) { + alfredClient.error(error); + } +})(); diff --git a/projects/packages/text-transformer/src/utils/format-table.util.ts b/projects/packages/text-transformer/src/utils/format-table.util.ts new file mode 100644 index 0000000..9717c2f --- /dev/null +++ b/projects/packages/text-transformer/src/utils/format-table.util.ts @@ -0,0 +1,98 @@ +export const formatMarkdownTable = (input: string): string => { + const lines = input.trim().split('\n'); + const tableLines: string[] = []; + let inTable = false; + let columnWidths: number[] = []; + + // First pass: identify tables and calculate column widths + for (const line of lines) { + if (line.trim().startsWith('|')) { + if (!inTable) { + inTable = true; + columnWidths = []; + } + + // Split by pipe and remove empty first/last elements + const cells = line + .split('|') + .slice(1, -1) + .map((cell) => cell.trim()); + + // Update column widths + cells.forEach((cell, index) => { + const cellLength = cell.replace(/^:?-+:?$/, '---').length; + columnWidths[index] = Math.max(columnWidths[index] || 0, cellLength); + }); + } else { + inTable = false; + } + } + + // Second pass: format tables with proper alignment + inTable = false; + let currentTableColumns: number[] = []; + + for (const line of lines) { + if (line.trim().startsWith('|')) { + if (!inTable) { + inTable = true; + currentTableColumns = []; + } + + const cells = line + .split('|') + .slice(1, -1) + .map((cell) => cell.trim()); + + // Update column widths for this table + if (currentTableColumns.length === 0) { + cells.forEach((cell, index) => { + const cellLength = cell.replace(/^:?-+:?$/, '---').length; + currentTableColumns[index] = Math.max(currentTableColumns[index] || 0, cellLength); + }); + + // Do another pass for all rows in this table + let tempIndex = lines.indexOf(line); + while (tempIndex < lines.length && lines[tempIndex]?.trim().startsWith('|')) { + const tempCells = lines[tempIndex] + ?.split('|') + .slice(1, -1) + .map((cell) => cell.trim()); + tempCells?.forEach((cell, idx) => { + const cellLength = cell.replace(/^:?-+:?$/, '---').length; + currentTableColumns[idx] = Math.max(currentTableColumns[idx] || 3, cellLength); + }); + tempIndex++; + } + } + + // Format cells with padding + const formattedCells = cells.map((cell, index) => { + const width = currentTableColumns[index] || 3; + + // Handle separator rows + if (cell.match(/^:?-+:?$/)) { + if (cell.startsWith(':') && cell.endsWith(':')) { + return ':' + '-'.repeat(Math.max(width - 2, 1)) + ':'; + } else if (cell.startsWith(':')) { + return ':' + '-'.repeat(Math.max(width - 1, 2)); + } else if (cell.endsWith(':')) { + return '-'.repeat(Math.max(width - 1, 2)) + ':'; + } else { + return '-'.repeat(Math.max(width, 3)); + } + } + + // Regular cells - pad with spaces + return cell.padEnd(width, ' '); + }); + + tableLines.push('| ' + formattedCells.join(' | ') + ' |'); + } else { + inTable = false; + tableLines.push(line); + } + } + + return tableLines.join('\n'); +};