Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<PropertyGroup>
<!-- https://aka.ms/vs-build-acceleration -->
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<RestoreUseStaticGraphEvaluation Condition="'$(RestoreUseStaticGraphEvaluation)' == ''">true</RestoreUseStaticGraphEvaluation>
</PropertyGroup>

<PropertyGroup Label="Build hygiene">
Expand Down
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- moves the duplicated MVC and Blazor citation-reference collector into shared `CrestApps.Core.AI.Chat` services as `CitationReferenceCollector`, registers it from `AddCoreAIChatInteractions()`, and lets hosts reuse the same citation-merging logic without copying sample code
- detects when uploaded chat-interaction or chat-session documents are being used for whole-document tasks such as summarization, review, rewrite, translation, or complete extraction work, and injects the full document text instead of relying only on chunk-level RAG
- upgrades the MVC sample host to Font Awesome 7 and adds draggable, resizable AI Chat widget layout persistence with a reset-size control that hosts can disable through widget config
- makes the shared `@crestapps/ai-chat-ui` message-action icons compatible with both Font Awesome CSS/webfont hosts and SVG+JS hosts that load `fontawesome-free/js/all.js`, so dynamically rendered playback and action buttons now appear correctly in Orchard-style integrations
- adds Chat History page listing previous sessions per AI Profile sorted by creation date, with resume, delete, delete-all, and new-chat actions
- adds Test page for Utility and Agent AI Profiles providing a single-prompt/single-response streamed UI
- renames the "Chat" button to "New Chat" on the AI Profile list and adds "Chat History" and "Test" buttons for applicable profile types
Expand Down
2 changes: 1 addition & 1 deletion src/CrestApps.Core.Docs/docs/core/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ Both widgets require the following libraries to be loaded on the page **before**
|---------|---------|-------------|
| [Chart.js](https://www.chartjs.org/) | Interactive chart rendering | `https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js` |
| [highlight.js](https://highlightjs.org/) | Code syntax highlighting | `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js` |
| [Font Awesome 7](https://fontawesome.com/) | Message action icons and brand icons such as Claude | `https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/css/all.min.css` |
| [Font Awesome 7](https://fontawesome.com/) | Message action icons and brand icons such as Claude; works with either the CSS/webfont include or the SVG+JS bundle | `https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/css/all.min.css` |

### Widget layout behavior

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -879,8 +879,8 @@ window.openAIChatWidgetManager = window.openAIChatWidgetManager || function () {
var buttonIndex = Number(button.getAttribute('data-tts-message-index'));
var isPlaying = buttonIndex === widgetApp.ttsPlayingMessageIndex;
var iconHtml = isPlaying
? '<span class="fa-solid fa-circle-pause" style="font-size: 0.9rem;"></span>'
: '<span class="fa-solid fa-circle-play" style="font-size: 0.9rem;"></span>';
? '<i class="fa-solid fa-circle-pause" style="font-size: 0.9rem;"></i>'
: '<i class="fa-solid fa-circle-play" style="font-size: 0.9rem;"></i>';

button.classList.toggle('tts-playing', isPlaying);
button.setAttribute('title', isPlaying ? 'Pause audio' : 'Read aloud');
Expand Down
95 changes: 74 additions & 21 deletions src/Resources/CrestApps.AI.Resources/Assets/js/ai-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ window.openAIChatManager = function () {
<div>
<div v-if="message.role === 'user'" class="ai-chat-msg-role ai-chat-msg-role-user">{{ userLabel }}</div>
<div v-else-if="message.role !== 'indicator'" :class="getAssistantRoleClasses(message)">
<span :class="getAssistantIconClasses(message, index)"><span :class="getAssistantIcon(message)"></span></span>
<span :class="getAssistantIconClasses(message, index)"><i :class="getAssistantIcon(message)"></i></span>
{{ getAssistantLabel(message) }}
</div>
<div class="ai-chat-message-body lh-base">
Expand All @@ -54,34 +54,34 @@ window.openAIChatManager = function () {
<template v-if="metricsEnabled && message.role === 'assistant'">
<span class="ai-chat-message-assistant-feedback" :data-message-id="message.id">
<button class="btn btn-sm btn-link text-success p-0 me-2 button-message-toolbox rate-up-btn" @click="rateMessage(message, true, $event)" :title="thumbsUpTitle">
<span class="fa-regular fa-thumbs-up"></span>
<i class="fa-regular fa-thumbs-up"></i>
</button>
<button class="btn btn-sm btn-link text-danger p-0 me-2 button-message-toolbox rate-down-btn" @click="rateMessage(message, false, $event)" :title="thumbsDownTitle">
<span class="fa-regular fa-thumbs-down"></span>
<i class="fa-regular fa-thumbs-down"></i>
</button>
</span>
</template>
<button v-if="textToSpeechEnabled && !isConversationMode && message.role === 'assistant' && !message.isStreaming" class="btn btn-sm btn-link text-secondary p-0 me-1 button-message-toolbox" :class="{ 'tts-playing': ttsPlayingMessageIndex === index }" :data-tts-message-index="index" @click="toggleMessageTts(message, index)" :title="ttsPlayingMessageIndex === index ? 'Pause audio' : 'Read aloud'">
<span :class="ttsPlayingMessageIndex === index ? 'fa-solid fa-circle-pause' : 'fa-solid fa-circle-play'"></span>
<i :class="ttsPlayingMessageIndex === index ? 'fa-solid fa-circle-pause' : 'fa-solid fa-circle-play'"></i>
</button>
<button class="btn btn-sm btn-link text-secondary p-0 button-message-toolbox" @click="copyResponse(message)" :title="copyTitle">
<span class="fa-solid fa-copy"></span>
<i class="fa-solid fa-copy"></i>
</button>
</span>
</div>
</div>
</div>
<div v-for="notification in notifications" :key="'notif-' + notification.type" class="ai-chat-notification" :class="'ai-chat-notification-' + (notification.type || 'info') + ' ' + (notification.cssClass || '')">
<div class="ai-chat-notification-content">
<span v-if="notification.icon" :class="notification.icon" class="ai-chat-notification-icon"></span>
<i v-if="notification.icon" :class="notification.icon" class="ai-chat-notification-icon"></i>
<span class="ai-chat-notification-text">{{ notification.content }}</span>
<button v-if="notification.dismissible" class="btn btn-sm btn-link p-0 ms-2 ai-chat-notification-dismiss" @click="dismissNotification(notification.type)" title="Dismiss">
<span class="fa-solid fa-xmark"></span>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div v-if="notification.actions && notification.actions.length" class="ai-chat-notification-actions">
<button v-for="action in notification.actions" :key="action.name" class="btn btn-sm" :class="action.cssClass || 'btn-outline-secondary'" @click="handleNotificationAction(notification.type, action.name)">
<span v-if="action.icon" :class="action.icon" class="me-1"></span>
<i v-if="action.icon" :class="action.icon" class="me-1"></i>
{{ action.label }}
</button>
</div>
Expand All @@ -90,7 +90,7 @@ window.openAIChatManager = function () {
`,
indicatorTemplate: `
<div class="ai-chat-msg-role ai-chat-msg-role-assistant">
<span class="ai-streaming-icon"><span class="fa fa-robot" style="display: inline-block;"></span></span>
<span class="ai-streaming-icon"><i class="fa fa-robot" style="display: inline-block;"></i></span>
Assistant
</div>
`
Expand All @@ -113,6 +113,49 @@ window.openAIChatManager = function () {
return span.innerHTML;
}

function containsFontAwesomePlaceholders(root) {
if (!root || root.nodeType !== 1) {
return false;
}

var selector = 'i[class*="fa-"],i.fa,i.fas,i.far,i.fab';
if (typeof root.matches === 'function' && root.matches(selector)) {
return true;
}

return typeof root.querySelector === 'function' && !!root.querySelector(selector);
}

function refreshFontAwesomeIcons(root) {
var fontAwesome = window.FontAwesome;
if (!containsFontAwesomePlaceholders(root) || !fontAwesome || !fontAwesome.dom || typeof fontAwesome.dom.i2svg !== 'function') {
return;
}

fontAwesome.dom.i2svg({ node: root });
}

function observeFontAwesomeIcons(root) {
if (!root || typeof MutationObserver === 'undefined') {
return null;
}

var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
refreshFontAwesomeIcons(node);
});
});
});

observer.observe(root, {
childList: true,
subtree: true
});

return observer;
}

function parsePixelValue(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
Expand Down Expand Up @@ -301,7 +344,7 @@ window.openAIChatManager = function () {
}

var langDisplay = lang ? escapeHtmlEntities(lang) : 'code';
return `<div class="ai-code-block"><div class="ai-code-header"><span class="ai-code-lang"><span class="fa-solid fa-code"></span> ${langDisplay}</span><button type="button" class="ai-code-copy-btn" title="Copy code"><span class="fa-regular fa-copy"></span></button></div><pre><code class="hljs${lang ? ' language-' + lang : ''}">${highlighted}</code></pre></div>`;
return `<div class="ai-code-block"><div class="ai-code-header"><span class="ai-code-lang"><i class="fa-solid fa-code"></i> ${langDisplay}</span><button type="button" class="ai-code-copy-btn" title="Copy code"><i class="fa-regular fa-copy"></i></button></div><pre><code class="hljs${lang ? ' language-' + lang : ''}">${highlighted}</code></pre></div>`;
};

// Custom image renderer for generated images with thumbnail styling and download button.
Expand All @@ -315,7 +358,7 @@ window.openAIChatManager = function () {
<img src="${src}" alt="${alt}" class="img-thumbnail" style="max-width: ${maxWidth}px; height: auto;" />
<div class="mt-2">
<a href="${src}" target="_blank" download="${alt}" title="${defaultConfig.downloadImageTitle}" class="btn btn-sm btn-outline-secondary ai-download-image">
<span class="fa-solid fa-download"></span>
<i class="fa-solid fa-download"></i>
</a>
</div>
</div>`;
Expand All @@ -338,7 +381,7 @@ window.openAIChatManager = function () {
+ `</div>`
+ `<div class="mt-2">`
+ `<button type="button" class="btn btn-sm btn-outline-secondary download-chart-btn" data-chart-id="${chartId}" title="${defaultConfig.downloadChartTitle}">`
+ `<span class="fa-solid fa-download"></span> ${defaultConfig.downloadChartButtonText}`
+ `<i class="fa-solid fa-download"></i> ${defaultConfig.downloadChartButtonText}`
+ `</button>`
+ `</div>`;
}
Expand Down Expand Up @@ -845,7 +888,7 @@ window.openAIChatManager = function () {
var name = doc.fileName || 'Document';
if (name.length > 20) name = name.substring(0, 17) + '...';
html += '<span class="badge bg-secondary bg-opacity-25 text-dark d-inline-flex align-items-center gap-1 px-2 py-1" style="font-size: 0.8rem;" title="' + this.escapeHtml(doc.fileName || '') + '">';
html += '<span class="fa-solid fa-file-lines" style="font-size: 0.7rem;"></span> ';
html += '<i class="fa-solid fa-file-lines" style="font-size: 0.7rem;"></i> ';
html += this.escapeHtml(name);
html += ' <button type="button" class="btn-close btn-close-sm ms-1" style="font-size: 0.5rem;" data-doc-index="' + i + '" aria-label="Remove"' + (this.isDocumentOperationPending ? ' disabled' : '') + '></button>';
html += '</span>';
Expand All @@ -857,7 +900,7 @@ window.openAIChatManager = function () {
var errorMsg = failedItem.error || 'Upload failed';
if (failedName.length > 15) failedName = failedName.substring(0, 12) + '...';
html += '<span class="badge bg-danger bg-opacity-25 text-danger d-inline-flex align-items-center gap-1 px-2 py-1" style="font-size: 0.8rem;" title="' + this.escapeHtml((failedItem.fileName || '') + ': ' + errorMsg) + '">';
html += '<span class="fa-solid fa-circle-exclamation" style="font-size: 0.7rem;"></span> ';
html += '<i class="fa-solid fa-circle-exclamation" style="font-size: 0.7rem;"></i> ';
html += this.escapeHtml(failedName);
html += ' <button type="button" class="btn-close btn-close-sm ms-1" style="font-size: 0.5rem;" data-error-index="' + m + '" aria-label="Dismiss"></button>';
html += '</span>';
Expand All @@ -870,7 +913,7 @@ window.openAIChatManager = function () {
}

html += '<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill ai-chat-doc-add-btn d-inline-flex align-items-center gap-1" style="font-size: 0.75rem; padding: 0.15rem 0.5rem;" title="Attach documents"' + (this.isDocumentOperationPending ? ' disabled' : '') + '>';
html += '<span class="fa-solid fa-paperclip"></span>';
html += '<i class="fa-solid fa-paperclip"></i>';
if (this.documents.length === 0 && !this.isUploading) {
html += ' <span>Attach files</span>';
}
Expand Down Expand Up @@ -1453,10 +1496,10 @@ window.openAIChatManager = function () {

if (this.isRecording) {
this.micButton.classList.add('stt-recording');
this.micButton.innerHTML = '<span class="fa-solid fa-stop"></span>';
this.micButton.innerHTML = '<i class="fa-solid fa-stop"></i>';
} else {
this.micButton.classList.remove('stt-recording');
this.micButton.innerHTML = '<span class="fa-solid fa-microphone"></span>';
this.micButton.innerHTML = '<i class="fa-solid fa-microphone"></i>';
}
},
streamMessage(profileId, trimmedPrompt, sessionProfileId) {
Expand Down Expand Up @@ -1671,8 +1714,8 @@ window.openAIChatManager = function () {
var buttonIndex = Number(button.getAttribute('data-tts-message-index'));
var isPlaying = buttonIndex === this.ttsPlayingMessageIndex;
var iconHtml = isPlaying
? '<span class="fa-solid fa-circle-pause"></span>'
: '<span class="fa-solid fa-circle-play"></span>';
? '<i class="fa-solid fa-circle-pause"></i>'
: '<i class="fa-solid fa-circle-play"></i>';

button.classList.toggle('tts-playing', isPlaying);
button.setAttribute('title', isPlaying ? 'Pause audio' : 'Read aloud');
Expand Down Expand Up @@ -2390,9 +2433,9 @@ window.openAIChatManager = function () {
if (codeEl) {
navigator.clipboard.writeText(codeEl.textContent);
var copiedText = config.codeCopiedText || 'Copied!';
btn.innerHTML = '<span class="fa-solid fa-check"></span> ' + copiedText;
btn.innerHTML = '<i class="fa-solid fa-check"></i> ' + copiedText;
setTimeout(() => {
btn.innerHTML = '<span class="fa-regular fa-copy"></span>';
btn.innerHTML = '<i class="fa-regular fa-copy"></i>';
}, 2000);
}
});
Expand Down Expand Up @@ -2590,6 +2633,11 @@ window.openAIChatManager = function () {
if (isInitialized && hasWidgetConfig && widgetBehavior && typeof widgetBehavior.onMounted === 'function') {
widgetBehavior.onMounted(this, config);
}

this.$nextTick(() => {
refreshFontAwesomeIcons(this.$el);
this.fontAwesomeObserver = observeFontAwesomeIcons(this.$el);
});
})();

window.addEventListener('beforeunload', this.handleBeforeUnload);
Expand All @@ -2599,6 +2647,11 @@ window.openAIChatManager = function () {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('crestapps-ai-chat-stop-tts', this.handleExternalTtsStop);

if (this.fontAwesomeObserver) {
this.fontAwesomeObserver.disconnect();
this.fontAwesomeObserver = null;
}

this.stopAudio(false);

if (this.stream) {
Expand Down
Loading
Loading