added ai-agent
This commit is contained in:
parent
b458e11e90
commit
c0e44d14ff
4 changed files with 373 additions and 27 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
package ai.dynamixs;
|
package ai.dynamixs.agent;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
@ -10,24 +10,19 @@ import org.imixs.workflow.exceptions.ModelException;
|
||||||
import org.imixs.workflow.exceptions.PluginException;
|
import org.imixs.workflow.exceptions.PluginException;
|
||||||
import org.imixs.workflow.faces.data.WorkflowController;
|
import org.imixs.workflow.faces.data.WorkflowController;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.enterprise.context.SessionScoped;
|
||||||
import jakarta.enterprise.context.Conversation;
|
|
||||||
import jakarta.enterprise.context.ConversationScoped;
|
|
||||||
import jakarta.enterprise.event.Observes;
|
import jakarta.enterprise.event.Observes;
|
||||||
import jakarta.enterprise.event.Reception;
|
import jakarta.enterprise.event.Reception;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
|
|
||||||
@Named("aiAgent")
|
@Named("aiAgentController")
|
||||||
@ConversationScoped
|
@SessionScoped
|
||||||
public class AIAgentController implements Serializable {
|
public class AIAgentController implements Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
private static Logger logger = Logger.getLogger(AIAgentController.class.getName());
|
private static Logger logger = Logger.getLogger(AIAgentController.class.getName());
|
||||||
|
|
||||||
@Inject
|
|
||||||
private Conversation conversation;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private OpenAIAPIService openAIAPIService;
|
private OpenAIAPIService openAIAPIService;
|
||||||
|
|
||||||
|
|
@ -37,6 +32,9 @@ public class AIAgentController implements Serializable {
|
||||||
@Inject
|
@Inject
|
||||||
protected WorkflowController workflowController;
|
protected WorkflowController workflowController;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected SkillController skillController;
|
||||||
|
|
||||||
private String userMessage;
|
private String userMessage;
|
||||||
|
|
||||||
private String agentResponse;
|
private String agentResponse;
|
||||||
|
|
@ -47,15 +45,46 @@ public class AIAgentController implements Serializable {
|
||||||
/**
|
/**
|
||||||
* Starts the conversation scope and initializes the context.
|
* Starts the conversation scope and initializes the context.
|
||||||
*/
|
*/
|
||||||
@PostConstruct
|
public void init() {
|
||||||
public void init(String systemPrompt) {
|
logger.info("├── Initialize AI Agent");
|
||||||
if (conversation.isTransient()) {
|
redirectUrl = null;
|
||||||
conversation.begin();
|
agentResponse = null;
|
||||||
}
|
contextHandler.init();
|
||||||
|
// Build system prompt with skill headers
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contextHandler.init();
|
contextHandler.addSystemMessage(
|
||||||
contextHandler.addSystemMessage(systemPrompt);
|
"You are a workflow assistant in Imixs-Office-Workflow. " +
|
||||||
|
"When a user wants to start a process, call start_process with the " +
|
||||||
|
"matching modelversion and taskId from the list below.\n\n" +
|
||||||
|
"Do NOT explain what you are doing. Just call the tool directly.\n\n" +
|
||||||
|
skillController.getSkillHeaders());
|
||||||
|
|
||||||
|
// Register start_process function
|
||||||
|
contextHandler.addFunction(
|
||||||
|
"start_process",
|
||||||
|
"Starts a BPMN workflow process for the user",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"$modelversion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The model version of the process"
|
||||||
|
},
|
||||||
|
"$taskid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The task ID of the initial task"
|
||||||
|
},
|
||||||
|
"process.ref": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unique ID referring of the process"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["$modelversion", "$taskid", "process.ref"]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
} catch (PluginException e) {
|
} catch (PluginException e) {
|
||||||
logger.warning("Failed to init AIAgent: " + e.getMessage());
|
logger.warning("Failed to init AIAgent: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
@ -65,11 +94,11 @@ public class AIAgentController implements Serializable {
|
||||||
* Main entry point for the frontend. Runs the agent loop and returns either a
|
* Main entry point for the frontend. Runs the agent loop and returns either a
|
||||||
* text response or a redirect URL.
|
* text response or a redirect URL.
|
||||||
*/
|
*/
|
||||||
public String chat() throws PluginException {
|
public String startChat() throws PluginException {
|
||||||
redirectUrl = null;
|
|
||||||
agentResponse = null;
|
|
||||||
|
|
||||||
logger.info("├── Starting AI Agent");
|
init();
|
||||||
|
|
||||||
|
logger.info("├── Starting new chat");
|
||||||
logger.info("│ ├── User Question: " + userMessage);
|
logger.info("│ ├── User Question: " + userMessage);
|
||||||
contextHandler.addQuestion(userMessage, null, null);
|
contextHandler.addQuestion(userMessage, null, null);
|
||||||
|
|
||||||
|
|
@ -94,7 +123,6 @@ public class AIAgentController implements Serializable {
|
||||||
|
|
||||||
// Check if a tool call produced a redirect URL
|
// Check if a tool call produced a redirect URL
|
||||||
if (redirectUrl != null) {
|
if (redirectUrl != null) {
|
||||||
conversation.end();
|
|
||||||
return redirectUrl; // JSF redirect
|
return redirectUrl; // JSF redirect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,9 +141,12 @@ public class AIAgentController implements Serializable {
|
||||||
|
|
||||||
workflowController.create(modelVersion, taskId, processRef);
|
workflowController.create(modelVersion, taskId, processRef);
|
||||||
|
|
||||||
String url = "/pages/workitems/workitem/" + "";
|
// Set redirect URL directly on this bean - startChat() will pick it up
|
||||||
// event.setRedirectUrl(url);
|
this.redirectUrl = "/pages/workitems/workitem.xhtml?id="
|
||||||
event.setResult("Process started: " + taskId);
|
+ workflowController.getWorkitem().getUniqueID()
|
||||||
|
+ "&faces-redirect=true";
|
||||||
|
|
||||||
|
event.setResult("New Process started: " + workflowController.getWorkitem().getUniqueID());
|
||||||
}
|
}
|
||||||
} catch (ModelException e) {
|
} catch (ModelException e) {
|
||||||
throw new PluginException(e.getErrorContext(), e.getErrorCode(), e.getMessage());
|
throw new PluginException(e.getErrorContext(), e.getErrorCode(), e.getMessage());
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
package ai.dynamixs.agent;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to convert HTML content into clean, LLM-friendly markup text.
|
||||||
|
*/
|
||||||
|
public class HtmlStripper {
|
||||||
|
|
||||||
|
// Precompiled patterns for performance
|
||||||
|
private static final Pattern P_SCRIPT_STYLE = Pattern.compile(
|
||||||
|
"<(script|style)[^>]*>[\\s\\S]*?</\\1>", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern P_HEADING = Pattern.compile(
|
||||||
|
"<h[1-5][^>]*>(.*?)</h[1-5]>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||||
|
private static final Pattern P_STRONG = Pattern.compile(
|
||||||
|
"<(strong|b)[^>]*>(.*?)</(strong|b)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||||
|
private static final Pattern P_EM = Pattern.compile(
|
||||||
|
"<(em|i)[^>]*>(.*?)</(em|i)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||||
|
private static final Pattern P_LI = Pattern.compile(
|
||||||
|
"<li[^>]*>(.*?)</li>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||||
|
private static final Pattern P_BLOCK = Pattern.compile(
|
||||||
|
"<(p|div|br|tr|blockquote|pre|ul|ol|table)[^>]*/?>",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern P_CLOSE_BLOCK = Pattern.compile(
|
||||||
|
"</(p|div|tr|blockquote|pre|ul|ol|table)>",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern P_TAGS = Pattern.compile("<[^>]+>");
|
||||||
|
private static final Pattern P_MULTI_NEWLINE = Pattern.compile("\\n{3,}");
|
||||||
|
private static final Pattern P_BLANK_LINE = Pattern.compile("(?m)^[ \\t]+$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an HTML string into clean, LLM-readable markdown-style text. Handles
|
||||||
|
* headings, bold/italic, lists, images, HTML entities and removes all remaining
|
||||||
|
* tags. Designed to be minimal and allocation-efficient.
|
||||||
|
*
|
||||||
|
* @param content raw HTML input (may be null)
|
||||||
|
* @return clean markup text, or empty string if input is null/blank
|
||||||
|
*/
|
||||||
|
public static String stripHtml(String content) {
|
||||||
|
if (content == null || content.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String s = content;
|
||||||
|
|
||||||
|
// 1. Normalize line endings
|
||||||
|
s = s.replace("\r\n", "\n").replace("\r", "\n");
|
||||||
|
|
||||||
|
// 2. Remove script / style blocks entirely
|
||||||
|
s = P_SCRIPT_STYLE.matcher(s).replaceAll("");
|
||||||
|
|
||||||
|
// 3. Images → [Image: alt] or drop silently
|
||||||
|
// s = P_IMG.matcher(s).replaceAll("[Image: $1]");
|
||||||
|
// s = P_IMG_NO_ALT.matcher(s).replaceAll("");
|
||||||
|
|
||||||
|
// 4. Headings → Markdown headings
|
||||||
|
s = P_HEADING.matcher(s).replaceAll("\n**$1**\n");
|
||||||
|
|
||||||
|
// 5. Bold / Italic → Markdown
|
||||||
|
s = P_STRONG.matcher(s).replaceAll("**$2**");
|
||||||
|
s = P_EM.matcher(s).replaceAll("*$2*");
|
||||||
|
|
||||||
|
// 6. List items → Markdown bullets
|
||||||
|
s = P_LI.matcher(s).replaceAll("\n- $1");
|
||||||
|
|
||||||
|
// 7. Block-level elements → newlines
|
||||||
|
s = P_BLOCK.matcher(s).replaceAll("\n");
|
||||||
|
s = P_CLOSE_BLOCK.matcher(s).replaceAll("\n");
|
||||||
|
|
||||||
|
// 8. Strip all remaining tags
|
||||||
|
s = P_TAGS.matcher(s).replaceAll("");
|
||||||
|
|
||||||
|
// 9. Decode HTML entities
|
||||||
|
s = decodeEntities(s);
|
||||||
|
|
||||||
|
// 10. Clean up whitespace
|
||||||
|
s = P_BLANK_LINE.matcher(s).replaceAll("");
|
||||||
|
s = P_MULTI_NEWLINE.matcher(s).replaceAll("\n\n");
|
||||||
|
|
||||||
|
return s.strip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes common named and numeric HTML entities. Covers the full ISO-8859-1 /
|
||||||
|
* HTML4 named entity set plus numeric references.
|
||||||
|
*/
|
||||||
|
private static String decodeEntities(String s) {
|
||||||
|
|
||||||
|
// Use a simple regex-replace loop for correctness
|
||||||
|
s = decodeNumericEntities(s);
|
||||||
|
|
||||||
|
// Named entities – most frequent ones in German/English business docs
|
||||||
|
s = s
|
||||||
|
.replace(" ", " ")
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace(""", "\"")
|
||||||
|
.replace("'", "'")
|
||||||
|
.replace("ä", "ä").replace("Ä", "Ä")
|
||||||
|
.replace("ö", "ö").replace("Ö", "Ö")
|
||||||
|
.replace("ü", "ü").replace("Ü", "Ü")
|
||||||
|
.replace("ß", "ß")
|
||||||
|
.replace("é", "é").replace("É", "É")
|
||||||
|
.replace("è", "è").replace("È", "È")
|
||||||
|
.replace("ê", "ê").replace("Ê", "Ê")
|
||||||
|
.replace("à", "à").replace("À", "À")
|
||||||
|
.replace("â", "â").replace("Â", "Â")
|
||||||
|
.replace("ô", "ô").replace("Ô", "Ô")
|
||||||
|
.replace("û", "û").replace("Û", "Û")
|
||||||
|
.replace("ç", "ç").replace("Ç", "Ç")
|
||||||
|
.replace("ñ", "ñ").replace("Ñ", "Ñ")
|
||||||
|
.replace("©", "©")
|
||||||
|
.replace("®", "®")
|
||||||
|
.replace("™", "™")
|
||||||
|
.replace("—", "—")
|
||||||
|
.replace("–", "–")
|
||||||
|
.replace("…", "…")
|
||||||
|
.replace("«", "«").replace("»", "»")
|
||||||
|
.replace("•", "•")
|
||||||
|
.replace("€", "€");
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Pattern P_DEC_ENTITY = Pattern.compile("&#(\\d+);");
|
||||||
|
private static final Pattern P_HEX_ENTITY = Pattern.compile("&#x([0-9a-fA-F]+);");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces numeric HTML entities (decimal and hex) with their Unicode
|
||||||
|
* characters.
|
||||||
|
*/
|
||||||
|
private static String decodeNumericEntities(String s) {
|
||||||
|
// Hex
|
||||||
|
var m = P_HEX_ENTITY.matcher(s);
|
||||||
|
var sb = new StringBuilder(); // ← StringBuilder statt StringBuffer
|
||||||
|
while (m.find()) {
|
||||||
|
int cp = Integer.parseInt(m.group(1), 16);
|
||||||
|
m.appendReplacement(sb, new String(Character.toChars(cp)));
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
s = sb.toString();
|
||||||
|
|
||||||
|
// Decimal
|
||||||
|
m = P_DEC_ENTITY.matcher(s);
|
||||||
|
sb = new StringBuilder(); // ← StringBuilder
|
||||||
|
while (m.find()) {
|
||||||
|
int cp = Integer.parseInt(m.group(1));
|
||||||
|
m.appendReplacement(sb, new String(Character.toChars(cp)));
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* Imixs Workflow
|
||||||
|
* Copyright (C) 2001, 2011 Imixs Software Solutions GmbH,
|
||||||
|
* http://www.imixs.com
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You can receive a copy of the GNU General Public
|
||||||
|
* License at http://www.gnu.org/licenses/gpl.html
|
||||||
|
*
|
||||||
|
* Project:
|
||||||
|
* http://www.imixs.org
|
||||||
|
* http://java.net/projects/imixs-workflow
|
||||||
|
*
|
||||||
|
* Contributors:
|
||||||
|
* Imixs Software Solutions GmbH - initial API and implementation
|
||||||
|
* Ralph Soika - Software Developer
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package ai.dynamixs.agent;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.imixs.marty.team.TeamController;
|
||||||
|
import org.imixs.workflow.ItemCollection;
|
||||||
|
import org.imixs.workflow.engine.DocumentEvent;
|
||||||
|
import org.imixs.workflow.office.model.ModelController;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.ejb.SessionContext;
|
||||||
|
import jakarta.enterprise.context.SessionScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SkillController is a CDI bean handling workflow skills from the current
|
||||||
|
* process models.
|
||||||
|
*
|
||||||
|
* @author rsoika
|
||||||
|
*/
|
||||||
|
@SessionScoped
|
||||||
|
public class SkillController implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TeamController teamController;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ModelController modelController;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
SessionContext ctx;
|
||||||
|
|
||||||
|
String skillHeaders = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method refreshes the cache of skillHeaders
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public void onDocumentEvent(@Observes DocumentEvent event) {
|
||||||
|
|
||||||
|
String type = event.getDocument().getType();
|
||||||
|
|
||||||
|
if ((type.startsWith("process") || type.startsWith("model"))
|
||||||
|
|
||||||
|
&& (event.getEventType() == DocumentEvent.ON_DOCUMENT_SAVE
|
||||||
|
|| event.getEventType() == DocumentEvent.ON_DOCUMENT_DELETE)) {
|
||||||
|
|
||||||
|
skillHeaders = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current skill headers. The method supports a lazy loading
|
||||||
|
* mechanism.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getSkillHeaders() {
|
||||||
|
loadSkillHeaders();
|
||||||
|
return skillHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to load the skill headers for all available processes and
|
||||||
|
* assigned workflows
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private void loadSkillHeaders() {
|
||||||
|
if (skillHeaders == null) {
|
||||||
|
StringBuilder skillHeaderBuilder = new StringBuilder();
|
||||||
|
skillHeaderBuilder.append("Available Workflow Processes:\n\n");
|
||||||
|
|
||||||
|
List<ItemCollection> processList = teamController.getProcessList();
|
||||||
|
|
||||||
|
for (ItemCollection process : processList) {
|
||||||
|
if (process.getTaskID() == 301 || teamController.isManagerOf(process.getUniqueID())) {
|
||||||
|
|
||||||
|
String processName = process.getItemValueString("name");
|
||||||
|
String processDescription = HtmlStripper
|
||||||
|
.stripHtml(process.getItemValueString("txtdescription"));
|
||||||
|
|
||||||
|
List<String> workflowList = process.getItemValueList("txtWorkflowList", String.class);
|
||||||
|
|
||||||
|
// Collect workflow entries first - skip process if none found
|
||||||
|
StringBuilder workflowEntries = new StringBuilder();
|
||||||
|
for (String group : workflowList) {
|
||||||
|
String modelVersion = modelController.getVersionByGroup(group);
|
||||||
|
List<ItemCollection> startTasks = modelController.findAllStartTasksByGroup(modelVersion, group);
|
||||||
|
if (!startTasks.isEmpty()) {
|
||||||
|
ItemCollection initialTask = startTasks.get(0);
|
||||||
|
int taskId = initialTask.getItemValueInteger("taskid");
|
||||||
|
String taskDescription = HtmlStripper.stripHtml(
|
||||||
|
modelController.getProcessDescriptionByInitialTask(initialTask, modelVersion,
|
||||||
|
group));
|
||||||
|
|
||||||
|
workflowEntries.append("- ").append(group).append(": ");
|
||||||
|
if (taskDescription != null && !taskDescription.isBlank()) {
|
||||||
|
workflowEntries.append(taskDescription);
|
||||||
|
} else if (processDescription != null && !processDescription.isBlank()) {
|
||||||
|
workflowEntries.append(processDescription);
|
||||||
|
}
|
||||||
|
workflowEntries.append(" (model: ").append(modelVersion)
|
||||||
|
.append(", taskid: ").append(taskId)
|
||||||
|
.append(", process.ref: ").append(process.getUniqueID())
|
||||||
|
.append(")\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add process section if it has at least one workflow
|
||||||
|
if (workflowEntries.length() > 0) {
|
||||||
|
skillHeaderBuilder.append("[").append(processName).append("]");
|
||||||
|
if (processDescription != null && !processDescription.isBlank()) {
|
||||||
|
skillHeaderBuilder.append(" - ").append(processDescription);
|
||||||
|
}
|
||||||
|
skillHeaderBuilder.append("\n");
|
||||||
|
skillHeaderBuilder.append(workflowEntries);
|
||||||
|
skillHeaderBuilder.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skillHeaders = skillHeaderBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
<f:view>
|
<f:view>
|
||||||
<div class="toolbar agent">
|
<div class="toolbar agent">
|
||||||
<h:inputText a:data-id="nav_input_phrase" style="margin-right: 10px;" a:placeholder="What do you need?"
|
<h:inputText a:data-id="nav_input_phrase" style="margin-right: 10px;" a:placeholder="What do you need?"
|
||||||
value="#{aiAgent.userMessage}" />
|
value="#{aiAgentController.userMessage}" />
|
||||||
<h:commandLink action="#{aiAgent.chat()}" a:data-id="nav_search_link"><span class="typcn typcn-lightbulb"
|
<h:commandLink action="#{aiAgentController.startChat()}" a:data-id="nav_search_link"><span
|
||||||
style="font-size:1.4em;"></span>
|
class="typcn typcn-lightbulb" style="font-size:1.4em;"></span>
|
||||||
</h:commandLink>
|
</h:commandLink>
|
||||||
</div>
|
</div>
|
||||||
</f:view>
|
</f:view>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue