Generate Excalidraw elements from a text prompt using the LLM.
(
db: &SqlitePool,
prompt: &str,
existing_elements: Option<&str>,
provider_id: Option<&str>,
model_id: Option<&str>,
)
| 1092 | |
| 1093 | /// Generate Excalidraw elements from a text prompt using the LLM. |
| 1094 | pub async fn generate_excalidraw( |
| 1095 | db: &SqlitePool, |
| 1096 | prompt: &str, |
| 1097 | existing_elements: Option<&str>, |
| 1098 | provider_id: Option<&str>, |
| 1099 | model_id: Option<&str>, |
| 1100 | ) -> AppResult<String> { |
| 1101 | let provider = provider_service::get_provider_for_chat(db, provider_id).await?; |
| 1102 | let model = model_id.unwrap_or(&provider.model); |
| 1103 | |
| 1104 | let mut messages = vec![ |
| 1105 | serde_json::json!({"role": "system", "content": EXCALIDRAW_SYSTEM_PROMPT}), |
| 1106 | ]; |
| 1107 | |
| 1108 | // If there are existing elements, include them so AI can edit |
| 1109 | if let Some(elements) = existing_elements { |
| 1110 | messages.push(serde_json::json!({ |
| 1111 | "role": "user", |
| 1112 | "content": format!("Here are the current canvas elements:\n{}\n\nIMPORTANT: When I ask you to modify something, return ALL elements (modified + unmodified). Keep all existing element IDs, positions, and properties unless I specifically ask to change them.", elements) |
| 1113 | })); |
| 1114 | messages.push(serde_json::json!({ |
| 1115 | "role": "assistant", |
| 1116 | "content": "I understand. I'll return the complete set of elements, only modifying what you ask for while preserving everything else exactly as-is." |
| 1117 | })); |
| 1118 | } |
| 1119 | |
| 1120 | messages.push(serde_json::json!({"role": "user", "content": prompt})); |
| 1121 | |
| 1122 | let client = http_client::request_client()?; |
| 1123 | let endpoint = format!("{}/chat/completions", provider.base_url.trim_end_matches('/')); |
| 1124 | |
| 1125 | let payload = serde_json::json!({ |
| 1126 | "model": model, |
| 1127 | "messages": messages, |
| 1128 | "temperature": 0.3, |
| 1129 | "max_tokens": 4000, |
| 1130 | "stream": false, |
| 1131 | }); |
| 1132 | |
| 1133 | let mut request = client |
| 1134 | .post(&endpoint) |
| 1135 | .header(CONTENT_TYPE, "application/json") |
| 1136 | .json(&payload); |
| 1137 | |
| 1138 | if let Some(key) = provider.api_key.as_deref().filter(|k| !k.trim().is_empty()) { |
| 1139 | request = request.header(AUTHORIZATION, format!("Bearer {key}")); |
| 1140 | } |
| 1141 | |
| 1142 | let response = request.send().await?; |
| 1143 | if !response.status().is_success() { |
| 1144 | let status = response.status(); |
| 1145 | let body = response.text().await.unwrap_or_default(); |
| 1146 | return Err(AppError::Http(format!("{status}: {body}"))); |
| 1147 | } |
| 1148 | |
| 1149 | let body: Value = response.json().await?; |
| 1150 | let content = body["choices"][0]["message"]["content"] |
| 1151 | .as_str() |
nothing calls this directly
no test coverage detected