.
This commit is contained in:
@@ -80,7 +80,7 @@ fun main() = application {
|
||||
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
||||
val binPath = getLlamaBinPath()
|
||||
val windowState = rememberWindowState(
|
||||
placement = WindowPlacement.Fullscreen
|
||||
placement = WindowPlacement.Floating
|
||||
)
|
||||
Window(onCloseRequest = ::exitApplication, title = "KIS AI 자동매매", state = windowState) {
|
||||
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import kotlinx.serialization.Serializable
|
||||
import model.AppConfig
|
||||
import network.TradingDecision
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.javatime.datetime
|
||||
@@ -413,7 +414,7 @@ object TradingLogStore {
|
||||
}
|
||||
}
|
||||
|
||||
fun addLog(tradingDecision: TradingDecision , decision: String, log: String) {
|
||||
fun addLog(tradingDecision: TradingDecision, decision: String, log: String) {
|
||||
synchronized(this) {
|
||||
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
||||
decisionLogs.add(
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
// src/main/kotlin/network/RagService.kt
|
||||
package network// src/main/kotlin/network/RagService.kt
|
||||
|
||||
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
|
||||
import dev.langchain4j.data.document.Metadata
|
||||
import dev.langchain4j.data.message.UserMessage
|
||||
import dev.langchain4j.data.segment.TextSegment
|
||||
import dev.langchain4j.exception.InternalServerException
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel
|
||||
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
|
||||
import dev.langchain4j.service.AiServices
|
||||
import dev.langchain4j.service.SystemMessage
|
||||
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
|
||||
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import model.CandleData
|
||||
import network.DartCodeManager
|
||||
import network.FinancialMapper
|
||||
import network.FinancialStatement
|
||||
import network.NewsService
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.apache.lucene.store.MMapDirectory
|
||||
import org.slf4j.MDC.put
|
||||
import service.FinancialAnalyzer
|
||||
import service.InvestmentScores
|
||||
import service.TechnicalAnalyzer
|
||||
@@ -25,9 +38,10 @@ import service.TradingDecisionCallback
|
||||
import service.UrlCacheManager
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface TradingAnalyst {
|
||||
@dev.langchain4j.service.SystemMessage("""
|
||||
@SystemMessage("""
|
||||
You are a Senior Stock Analyst.
|
||||
Analyze the data and provide a decision in JSON format.
|
||||
You must respond ONLY with a valid JSON object.
|
||||
@@ -48,13 +62,13 @@ object RagService {
|
||||
.apiKey("unused")
|
||||
.temperature(0.0) // [중요] 0.0으로 설정하여 결정론적 응답 유도
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.frequencyPenalty(1.1)
|
||||
.maxTokens(500) // 👈 루프 방지를 위해 반드시 짧게 제한!
|
||||
// .frequencyPenalty(1.1)
|
||||
.maxTokens(400) // 👈 루프 방지를 위해 반드시 짧게 제한!
|
||||
// 1.x 버전에서는 responseFormat이 아래처럼 바뀔 수 있으니 체크하세요
|
||||
.responseFormat("json_object")
|
||||
.build()
|
||||
|
||||
private val analyst = dev.langchain4j.service.AiServices.builder(TradingAnalyst::class.java)
|
||||
private val analyst = AiServices.builder(TradingAnalyst::class.java)
|
||||
.chatModel(chatModel)
|
||||
.build()
|
||||
|
||||
@@ -139,8 +153,18 @@ object RagService {
|
||||
|
||||
object JsonSanitizer {
|
||||
fun formatJson(raw: String): String {
|
||||
// 실제 응답 로그 출력 (디버깅용)
|
||||
println("📥 [AI Raw Response]:\n$raw")
|
||||
|
||||
val regex = Regex("""\{.*\}""", RegexOption.DOT_MATCHES_ALL)
|
||||
return raw.trim()
|
||||
val match = regex.find(raw)?.value
|
||||
|
||||
if (match == null) {
|
||||
println("⚠️ [JsonSanitizer] JSON 형식을 찾을 수 없습니다.")
|
||||
return "{}" // 빈 객체라도 반환하여 EOF 방지
|
||||
}
|
||||
|
||||
return match.trim()
|
||||
.removePrefix("```json")
|
||||
.removePrefix("```")
|
||||
.removeSuffix("```")
|
||||
@@ -264,13 +288,80 @@ object RagService {
|
||||
//// println(response)
|
||||
// return response.aiMessage().text()
|
||||
// }
|
||||
private const val LLM_API_URL = "http://127.0.0.1:8080/v1/chat/completions"
|
||||
|
||||
private suspend fun callLlamaWithSchema(prompt: String): String {
|
||||
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
// 문자열 치환 대신 안전한 JSON 객체 빌더 사용
|
||||
val requestBodyJson = buildJsonObject {
|
||||
put("model", "local-model")
|
||||
put("temperature", 0.1) // 0.1 유지 (결정론적 응답)
|
||||
put("top_p", 0.9)
|
||||
put("max_tokens", 500)
|
||||
|
||||
putJsonArray("messages") {
|
||||
addJsonObject {
|
||||
put("role", "system")
|
||||
put("content", "You are a helpful AI financial analyst. You must output responses ONLY in valid JSON format.")
|
||||
}
|
||||
addJsonObject {
|
||||
put("role", "user")
|
||||
put("content", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 복잡한 json_schema를 지우고, 단순히 JSON 형식으로만 내보내라고 지시합니다.
|
||||
putJsonObject("response_format") {
|
||||
put("type", "json_object")
|
||||
}
|
||||
}.toString()
|
||||
println("requestBodyJson =>> $requestBodyJson")
|
||||
val request = Request.Builder()
|
||||
.url(LLM_API_URL)
|
||||
.post(requestBodyJson.toRequestBody(jsonMediaType))
|
||||
.build()
|
||||
|
||||
return kotlinx.coroutines.Dispatchers.IO.let {
|
||||
kotlinx.coroutines.withContext(it) {
|
||||
httpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) throw Exception("LLM API Error: ${response.code} ${response.message}")
|
||||
|
||||
val responseBody = response.body?.string() ?: "{}"
|
||||
val json = Json.parseToJsonElement(responseBody).jsonObject
|
||||
json["choices"]?.jsonArray?.get(0)?.jsonObject?.get("message")?.jsonObject?.get("content")?.jsonPrimitive?.content ?: "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val httpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.build()
|
||||
suspend fun decideTrading(
|
||||
stockName: String,
|
||||
scores: InvestmentScores, // 직접 계산한 점수 객체
|
||||
financialStmt: FinancialStatement, // 매핑된 재무 수치 객체
|
||||
tempDecision: TradingDecision
|
||||
): TradingDecision? {
|
||||
// 💡 1. 뉴스 데이터가 유효한지(100자 이상인지) 확인
|
||||
val validNews = tempDecision.newsContext?.takeIf { it.trim().length >= 100 }
|
||||
|
||||
// 💡 2. 동적 데이터 섹션 구성
|
||||
val newsDataSection = if (validNews != null) {
|
||||
"3. News Context: $validNews"
|
||||
} else {
|
||||
"3. News Context: None available. Base your decision ONLY on System Scores and Financials."
|
||||
}
|
||||
|
||||
// 💡 3. 동적 제약 조건 구성
|
||||
val newsConstraint = if (validNews != null) {
|
||||
"- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD)."
|
||||
} else {
|
||||
"- No news data is available. Rely strictly on Financials and System Scores for your 'decision' and 'reason'."
|
||||
}
|
||||
|
||||
val prompt = """
|
||||
# Task: Senior AI Investment Analyst
|
||||
Analyze the stock '$stockName' and determine the final trading decision based on the data below.
|
||||
@@ -278,51 +369,52 @@ Analyze the stock '$stockName' and determine the final trading decision based on
|
||||
# Data
|
||||
1. System Scores: Scalping(${scores.ultraShort}), Short(${scores.shortTerm}), Mid(${scores.midTerm}), Long(${scores.longTerm})
|
||||
2. Financials: Operating Profit ${if(financialStmt.isOperatingProfitPositive) "PROFIT" else "LOSS"} (Growth: ${"%.2f".format(financialStmt.operatingProfitGrowth)}%), ROE: ${"%.2f".format(financialStmt.roe)}%, Debt: ${"%.2f".format(financialStmt.debtRatio)}%
|
||||
3. News Context: ${tempDecision.newsContext?.take(400)} // 👈 뉴스 길이를 물리적으로 제한
|
||||
$newsDataSection
|
||||
|
||||
# Constraints
|
||||
1. 모든 점수와 confidence는 0에서 100 사이의 **정수(Integer)**로만 작성하십시오.
|
||||
- Copy the exact 'System Scores' from the Data section into the output JSON.
|
||||
- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD).
|
||||
- Synchronization: High scalping score + positive news momentum = Higher BUY confidence.
|
||||
- Output: Response ONLY in valid JSON format. No extra text.
|
||||
- The "reason" field MUST be written in KOREAN and MUST NOT exceed 50 characters. Keep it concise.
|
||||
- Output ONLY a valid JSON object matching the exact structure below. DO NOT output placeholder text like '(integer)'.
|
||||
|
||||
# Output JSON Format (Reason must be in Korean)
|
||||
# Example Output JSON Format
|
||||
{
|
||||
"ultraShortScore": ${scores.ultraShort},
|
||||
"shortTermScore": ${scores.shortTerm},
|
||||
"midTermScore": ${scores.midTerm},
|
||||
"longTermScore": ${scores.longTerm},
|
||||
"decision": "BUY/SELL/HOLD",
|
||||
"reason": "재무와 뉴스를 대조한 분석 결과 (한국어)",
|
||||
"confidence": 0~100
|
||||
"ultraShortScore": 0,
|
||||
"shortTermScore": 0,
|
||||
"midTermScore": 0,
|
||||
"longTermScore": 0,
|
||||
"decision": "HOLD",
|
||||
"reason": "적자 지속 및 네수파립 임상 대기 중으로 관망 필요",
|
||||
"confidence": 50
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val response = chatModel.chat(UserMessage.from(prompt))
|
||||
val rawResponse = response.aiMessage().text()
|
||||
val jsonResponse = JsonSanitizer.formatJson(rawResponse)
|
||||
// println("📥 [AI Raw JSON]:\n$jsonResponse")
|
||||
|
||||
// 2. 유연한 파서 설정 (소수점 및 예외 상황 대응)
|
||||
val lenientJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
|
||||
// JSON 파싱 (Kotlinx Serialization 활용)
|
||||
return try {
|
||||
// println(jsonResponse)
|
||||
val decision = lenientJson.decodeFromString<TradingDecision>(jsonResponse)
|
||||
decision.financialData = tempDecision.financialData
|
||||
decision.newsContext = tempDecision.newsContext
|
||||
decision.techSummary = tempDecision.techSummary
|
||||
decision.stockCode = tempDecision.stockCode
|
||||
decision.corpName = tempDecision.corpName
|
||||
decision.stockName = tempDecision.stockName
|
||||
val rawResponse = callLlamaWithSchema(prompt)
|
||||
println("📥 [AI Strict JSON]:\n$rawResponse")
|
||||
|
||||
// 엄격한 스키마가 적용되었으므로 JsonSanitizer 없이 바로 파싱 가능
|
||||
val lenientJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
val decision = lenientJson.decodeFromString<TradingDecision>(rawResponse)
|
||||
|
||||
// 데이터 매핑
|
||||
decision.apply {
|
||||
financialData = tempDecision.financialData
|
||||
newsContext = tempDecision.newsContext
|
||||
techSummary = tempDecision.techSummary
|
||||
stockCode = tempDecision.stockCode
|
||||
corpName = tempDecision.corpName
|
||||
this.stockName = tempDecision.stockName
|
||||
}
|
||||
|
||||
decision
|
||||
} catch (e: dev.langchain4j.exception.InternalServerException) {
|
||||
} catch (e: InternalServerException) {
|
||||
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
||||
println("🚨 [AI Server Error] ${e.message}")
|
||||
if (e.message?.contains("Context size") == true) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package service
|
||||
|
||||
import AutoTradeItem
|
||||
import TradingDecision
|
||||
import network.TradingDecision
|
||||
import TradingLogStore
|
||||
import androidx.compose.runtime.remember
|
||||
import getLlamaBinPath
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,11 +26,11 @@ import model.RankingStock
|
||||
import model.RankingType
|
||||
import model.UnifiedBalance
|
||||
import network.DartCodeManager
|
||||
import network.FinancialMapper
|
||||
import network.FinancialStatement
|
||||
import network.KisAuthService
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import network.RagService
|
||||
import network.StockUniverseLoader
|
||||
import util.MarketUtil
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@@ -2,13 +2,9 @@ package service
|
||||
|
||||
import com.microsoft.playwright.Browser
|
||||
import com.microsoft.playwright.Playwright
|
||||
import com.microsoft.playwright.BrowserType
|
||||
import com.microsoft.playwright.Page
|
||||
import com.microsoft.playwright.options.LoadState
|
||||
import com.microsoft.playwright.options.WaitUntilState
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -18,6 +14,7 @@ import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import model.NewsItem
|
||||
import network.CorpInfo
|
||||
import network.RagService
|
||||
import java.net.URL
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import network.RagService
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
@@ -31,7 +32,7 @@ object LlamaServerManager {
|
||||
else -> 0 to 4 // 인텔 맥 2017 등
|
||||
}
|
||||
|
||||
val command = listOf(
|
||||
val command = mutableListOf(
|
||||
binPath,
|
||||
"-m", modelPath,
|
||||
"--port", port.toString(),
|
||||
@@ -40,7 +41,13 @@ object LlamaServerManager {
|
||||
"-t", threads.toString(),
|
||||
"--embedding"
|
||||
)
|
||||
|
||||
if (port != 8081) { // 텍스트 생성용 모델에만 적용
|
||||
command.addAll(listOf(
|
||||
"-b", "512", // Batch size (토큰 병렬 처리량 제한으로 연산 안정화)
|
||||
"--threads-batch", threads.toString(),
|
||||
"-fa","on" // Flash Attention 활성화 (메모리 절약 및 긴 컨텍스트 연산 안정성 증가)
|
||||
))
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
val pb = ProcessBuilder(command)
|
||||
|
||||
@@ -59,18 +59,25 @@ object SystemSleepPreventer {
|
||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||
*/
|
||||
fun start() {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
val arch = System.getProperty("os.arch").lowercase()
|
||||
val isWin = os.contains("win")
|
||||
val root = LoggerFactory.getLogger("Exposed") as Logger
|
||||
root.level = Level.ERROR
|
||||
checkAndRequestAccessibility()
|
||||
if (process?.isAlive == true) return
|
||||
if (!isWin) {
|
||||
checkAndRequestAccessibility()
|
||||
}
|
||||
|
||||
try {
|
||||
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
||||
val command = listOf("caffeinate", "-i", "-d", "-m")
|
||||
process = ProcessBuilder(command).start()
|
||||
println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
||||
if (process?.isAlive == true) return
|
||||
if (!isWin) {
|
||||
try {
|
||||
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
||||
val command = listOf("caffeinate", "-i", "-d", "-m")
|
||||
process = ProcessBuilder(command).start()
|
||||
println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import TradingDecision
|
||||
import network.TradingDecision
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
@@ -19,10 +19,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import model.CandleData
|
||||
import model.ConfigIndex
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import TradingDecision
|
||||
import network.TradingDecision
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.min
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.ConfigIndex
|
||||
|
||||
@@ -128,32 +128,60 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||
){
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val path = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,)
|
||||
val rawUri = data.readFiles().firstOrNull()
|
||||
if (rawUri != null) {
|
||||
// 1. file:// 또는 file: 접두사 제거
|
||||
var path = rawUri.removePrefix("file://").removePrefix("file:")
|
||||
|
||||
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
|
||||
if (path.startsWith("/") && path.getOrNull(2) == ':') {
|
||||
path = path.drop(1)
|
||||
}
|
||||
|
||||
if (path.endsWith(".gguf")) config = config.copy(modelPath = path)
|
||||
}
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp)
|
||||
Text(
|
||||
if (config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,)
|
||||
val rawUri = data.readFiles().firstOrNull()
|
||||
if (rawUri != null) {
|
||||
// 1. file:// 또는 file: 접두사 제거
|
||||
var embedModelPath = rawUri.removePrefix("file://").removePrefix("file:")
|
||||
|
||||
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
|
||||
if (embedModelPath.startsWith("/") && embedModelPath.getOrNull(2) == ':') {
|
||||
embedModelPath = embedModelPath.drop(1)
|
||||
}
|
||||
|
||||
if (embedModelPath.endsWith(".gguf")) config =
|
||||
config.copy(embedModelPath = embedModelPath)
|
||||
}
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath, fontSize = 12.sp)
|
||||
Text(
|
||||
if (config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
@@ -2,7 +2,7 @@ package ui
|
||||
|
||||
|
||||
|
||||
import TradingDecision
|
||||
import network.TradingDecision
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
Reference in New Issue
Block a user