@@ -94,13 +94,14 @@ define([
9494 let voiceSessionRequestId = 0 ;
9595 /** @type {RegExp } SOLA follow-up marker parser */
9696 const NEXT_BLOCK_RE = / \n * \[ S O L A _ N E X T \] ( [ \s \S ] * ?) \[ \/ S O L A _ N E X T \] / ;
97- /** @type {RegExp } Source attribution tag parser */
98- const SOURCE_TAG_RE = / \n * \[ S O U R C E : ( p a g e | c o u r s e | g e n e r a l ) \] / ;
97+ /** @type {RegExp } Source attribution tag parser — matches [SOURCE:page], [SOURCE:course], [SOURCE:general], [SOURCE:activity:123] */
98+ const SOURCE_TAG_RE = / \n * \[ S O U R C E : ( p a g e | c o u r s e | g e n e r a l | a c t i v i t y ) (?: : ( \d + ) ) ? \] / ;
9999 /** @type {Object<string, string> } */
100100 const SOURCE_LABELS = {
101101 page : 'From: Current Page' ,
102102 course : 'From: Course Materials' ,
103103 general : 'General Knowledge' ,
104+ activity : 'From: Course Materials' ,
104105 } ;
105106 /** @type {string } Local intro-dismiss key */
106107 const INTRO_DISMISSED_KEY = 'ai_course_assistant_intro_dismissed' ;
@@ -113,12 +114,13 @@ define([
113114 * Parse SOLA metadata markers from an assistant response.
114115 *
115116 * @param {string } text
116- * @returns {{text:string,suggestions:Array<string>,sourceType:string|null} }
117+ * @returns {{text:string,suggestions:Array<string>,sourceType:string|null,sourceCmid:string|null } }
117118 */
118119 const parseAssistantDecorators = function ( text ) {
119120 let cleanText = ( ( text || '' ) + '' ) ;
120121 let suggestions = [ ] ;
121122 let sourceType = null ;
123+ let sourceCmid = null ;
122124
123125 const nextMatch = cleanText . match ( NEXT_BLOCK_RE ) ;
124126 if ( nextMatch ) {
@@ -131,13 +133,15 @@ define([
131133 const sourceMatch = cleanText . match ( SOURCE_TAG_RE ) ;
132134 if ( sourceMatch ) {
133135 sourceType = sourceMatch [ 1 ] ;
136+ sourceCmid = sourceMatch [ 2 ] || null ; // Numeric cmid for activity type.
134137 cleanText = cleanText . replace ( SOURCE_TAG_RE , '' ) . trimEnd ( ) ;
135138 }
136139
137140 return {
138141 text : cleanText ,
139142 suggestions : suggestions ,
140143 sourceType : sourceType ,
144+ sourceCmid : sourceCmid ,
141145 } ;
142146 } ;
143147
@@ -192,20 +196,34 @@ define([
192196 /**
193197 * Create a source attribution pill element (clickable link or plain span).
194198 *
195- * @param {string } sourceType 'page', 'course', or 'general'
196- * @param {Object|null } meta SSE metadata with pageurl, courseurl, pagetitle
199+ * @param {string } sourceType 'page', 'course', 'general', or 'activity'
200+ * @param {Object|null } meta SSE metadata with pageurl, courseurl, pagetitle, modules
201+ * @param {string|null } cmid Course module ID for activity source type
197202 * @returns {HTMLElement }
198203 */
199- const createSourcePill = function ( sourceType , meta ) {
204+ const createSourcePill = function ( sourceType , meta , cmid ) {
200205 var href = '' ;
206+ var label = SOURCE_LABELS [ sourceType ] || sourceType ;
201207 var title = '' ;
202- if ( sourceType === 'page' && meta && meta . pageurl ) {
208+
209+ if ( sourceType === 'activity' && cmid && meta && meta . modules && meta . modules [ cmid ] ) {
210+ var mod = meta . modules [ cmid ] ;
211+ href = mod . url ;
212+ label = 'From: ' + mod . title ;
213+ title = mod . title ;
214+ } else if ( sourceType === 'page' && meta && meta . pageurl ) {
203215 href = meta . pageurl ;
204216 title = meta . pagetitle || '' ;
205- } else if ( sourceType === 'course' && meta && meta . courseurl ) {
217+ } else if ( ( sourceType === 'course' || sourceType === 'activity' ) && meta && meta . courseurl ) {
206218 href = meta . courseurl ;
207- title = '' ;
208219 }
220+
221+ var linkIcon = ' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24"'
222+ + ' fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"'
223+ + ' stroke-linejoin="round" style="vertical-align:-1px;margin-left:2px">'
224+ + '<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/>'
225+ + '<polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>' ;
226+
209227 var pill ;
210228 if ( href ) {
211229 pill = document . createElement ( 'a' ) ;
@@ -215,18 +233,12 @@ define([
215233 if ( title ) {
216234 pill . title = title ;
217235 }
218- // Small external link icon after label.
219- pill . innerHTML = ( SOURCE_LABELS [ sourceType ] || sourceType )
220- + ' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24"'
221- + ' fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"'
222- + ' stroke-linejoin="round" style="vertical-align:-1px;margin-left:2px">'
223- + '<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/>'
224- + '<polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>' ;
236+ pill . innerHTML = label + linkIcon ;
225237 } else {
226238 pill = document . createElement ( 'span' ) ;
227- pill . textContent = SOURCE_LABELS [ sourceType ] || sourceType ;
239+ pill . textContent = label ;
228240 }
229- pill . className = 'aica-source-pill aica-source-pill--' + sourceType ;
241+ pill . className = 'aica-source-pill aica-source-pill--' + ( sourceType === 'activity' ? 'course' : sourceType ) ;
230242 return pill ;
231243 } ;
232244
@@ -2812,9 +2824,11 @@ define([
28122824 text : ( text || '' ) + '' ,
28132825 suggestions : [ ] ,
28142826 sourceType : options . sourceType || null ,
2827+ sourceCmid : options . sourceCmid || null ,
28152828 } : parseAssistantDecorators ( text ) ;
28162829 const displayText = parsed . text ;
28172830 const sourceType = options . sourceType || parsed . sourceType ;
2831+ const sourceCmid = options . sourceCmid || parsed . sourceCmid ;
28182832 if ( ! options . skipHistory ) {
28192833 recordConversationMessage ( 'assistant' , displayText , ts || Date . now ( ) ) ;
28202834 }
@@ -2828,7 +2842,7 @@ define([
28282842 pageurl : '' ,
28292843 pagetitle : ''
28302844 } ;
2831- el . appendChild ( createSourcePill ( sourceType , histMeta ) ) ;
2845+ el . appendChild ( createSourcePill ( sourceType , histMeta , sourceCmid ) ) ;
28322846 }
28332847 return el ;
28342848 } ;
@@ -3734,6 +3748,7 @@ define([
37343748 addAssistantMsg ( text , msg . timecreated ? msg . timecreated * 1000 : null , {
37353749 skipHistory : true ,
37363750 sourceType : parsed . sourceType ,
3751+ sourceCmid : parsed . sourceCmid ,
37373752 alreadyClean : true ,
37383753 } ) ;
37393754 } else {
@@ -3913,11 +3928,11 @@ define([
39133928 if ( fullText ) {
39143929 const parsed = parseAssistantDecorators ( fullText ) ;
39153930 UI . finishStreaming ( parsed . text , ( getTtsUrl ( ) || Speech . isTTSSupported ( ) ) ? handleSpeak : null ) ;
3916- // Append clickable source pill using SSE metadata (page/course URLs).
3931+ // Append clickable source pill using SSE metadata (page/course URLs + modules map ).
39173932 if ( parsed . sourceType ) {
39183933 var lastMsgEl = getLastAssistantMessageEl ( ) ;
39193934 if ( lastMsgEl ) {
3920- lastMsgEl . appendChild ( createSourcePill ( parsed . sourceType , streamMeta ) ) ;
3935+ lastMsgEl . appendChild ( createSourcePill ( parsed . sourceType , streamMeta , parsed . sourceCmid ) ) ;
39213936 }
39223937 }
39233938 recordConversationMessage ( 'assistant' , parsed . text , Date . now ( ) ) ;
@@ -3957,7 +3972,7 @@ define([
39573972 if ( parsed . sourceType ) {
39583973 var errLastMsgEl = getLastAssistantMessageEl ( ) ;
39593974 if ( errLastMsgEl ) {
3960- errLastMsgEl . appendChild ( createSourcePill ( parsed . sourceType , streamMeta ) ) ;
3975+ errLastMsgEl . appendChild ( createSourcePill ( parsed . sourceType , streamMeta , parsed . sourceCmid ) ) ;
39613976 }
39623977 }
39633978 recordConversationMessage ( 'assistant' , parsed . text , Date . now ( ) ) ;
0 commit comments