first commit

This commit is contained in:
2026-01-09 21:25:34 +09:00
parent c5b6c019e6
commit 891ce6f3a9
36 changed files with 1391 additions and 46 deletions

7
.idea/misc.xml generated
View File

@@ -6,4 +6,11 @@
<component name="ProjectType"> <component name="ProjectType">
<option name="id" value="Android" /> <option name="id" value="Android" />
</component> </component>
<component name="VisualizationToolProject">
<option name="state">
<ProjectState>
<option name="scale" value="0.201519775390625" />
</ProjectState>
</option>
</component>
</project> </project>

View File

@@ -51,6 +51,16 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-simplexml:2.9.0")
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-simplexml:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
}
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -22,6 +25,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".RegionSelectActivity" />
<activity android:name=".LineSelectActivity" />
<activity android:name=".LinePriorityActivity" />
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class AkaneResponder : CharacterResponder {
override val profile = CharacterProfiles.akane
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ!?遅延!? 急ぐなら別ルートも考えたほうがいいかもだよっ!!"
InfoType.REVISION ->
"うわーっ!ダイヤ改正だって!? 新しい運用とかワクワクするじゃんっ!"
InfoType.EVENT ->
"イベント列車!? こういうのめっちゃ楽しみだよねっ!!"
else ->
"なんか気になる情報があるよねっ!気をつけていこーっ!"
}
}
}

View File

@@ -0,0 +1,136 @@
package com.example.curation_train_app
object CharacterProfiles {
data class CharacterProfile(
val id: String,
val name: String,
val speakingStyle: String, // 話し方・口調の説明
val personality: String, // 性格・立ち位置
val knowledgeLevel: String // 鉄道知識レベル
)
// ───────────────────────────────
// 🔴1. 霊夢(メイン進行・解説)
// ───────────────────────────────
val reimu = CharacterProfile(
id = "reimu",
name = "霊夢",
speakingStyle =
"落ち着いた女言葉。断定を避け、やわらかい表現。敬語は使わない。" +
"語尾は自然な範囲で「〜よ」「〜わよ」「〜よね」を用いる。",
personality =
"穏やか、冷静、全体進行役。説明整理が得意で、視聴者に安心感を与える。優しい。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟡2. 魔理沙(補足・深掘り担当)
// ───────────────────────────────
val marisa = CharacterProfile(
id = "marisa",
name = "魔理沙",
speakingStyle =
"元気な口調。「〜だぜ」は乱発せず、強調・納得・気付きの場面でだけ自然に使う。",
personality =
"深掘り・解説の補足、技術寄りの説明が得意。少しテンション高めで勢いがある。",
knowledgeLevel = "高い鉄道知識"
)
// ───────────────────────────────
// 🟠3. フラン(明るく元気)
// ───────────────────────────────
val flan = CharacterProfile(
id = "flan",
name = "フラン",
speakingStyle =
"明るく元気。テンションが高いときだけ「なのだー」「のだー」が出る。" +
"普段は普通の話し方で語尾を毎回つけない。",
personality =
"ムードメーカー。感情表現が大きく、明るい。盛り上げ役。",
knowledgeLevel = "中〜低の鉄道知識(感覚派)"
)
// ───────────────────────────────
// 🟢4. 早苗(丁寧で落ち着いた補足役)
// ───────────────────────────────
val sanae = CharacterProfile(
id = "sanae",
name = "早苗",
speakingStyle =
"丁寧で落ち着いた語り。霊夢寄りの柔らかい話し方。敬語あり。",
personality =
"冷静な補足役。情景や背景知識を丁寧に伝える。視点が穏やか。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟨5. あかね(テンション高い・視聴者代表)
// ───────────────────────────────
val akane = CharacterProfile(
id = "akane",
name = "あかね",
speakingStyle =
"元気で明るい。テンション高め。語尾に小さい「っ」が入ることがある。" +
"自分のことを「HN君」と呼ぶ。",
personality =
"視聴者代表。リアクション担当。疑問を素直に口に出す。場を明るくする。",
knowledgeLevel = "低〜中の鉄道知識。難しい話は苦手。"
)
// ───────────────────────────────
// 🟦6. さやか(冷静・整理役)
// ───────────────────────────────
val sayaka = CharacterProfile(
id = "sayaka",
name = "さやか",
speakingStyle =
"落ち着いた女言葉。霊夢寄りの丁寧な話し方。語尾は崩れない。敬語は使わない",
personality =
"冷静な説明・整理役。感情を抑えめに、的確に指摘する。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟧7. ひより(穏やか・控えめ)
// ───────────────────────────────
val hiyori = CharacterProfile(
id = "hiyori",
name = "ひより",
speakingStyle =
"やわらかく、控えめな話し方。語彙は優しめ。崩れすぎない。",
personality =
"優しく控えめで、聞き役になることもある。しっかり者だが遠慮がち。",
knowledgeLevel = "中〜低の鉄道知識"
)
// ───────────────────────────────
// 🍑8. ももか(甘めの口調・テンション高め)
// ───────────────────────────────
val momoka = CharacterProfile(
id = "momoka",
name = "ももか",
speakingStyle =
"かわいめの語尾・柔らかい話し方。テンション高めの時だけ「っ」が入る。",
personality =
"甘めの雰囲気で明るい。感情が前に出るタイプ。",
knowledgeLevel = "低〜中の鉄道知識"
)
// ───────────────────────────────
// ID → キャラ検索
// ───────────────────────────────
fun getProfile(id: String): CharacterProfile? {
return when (id.lowercase()) {
"reimu" -> reimu
"marisa" -> marisa
"flan" -> flan
"sanae" -> sanae
"akane" -> akane
"sayaka" -> sayaka
"hiyori" -> hiyori
"momoka" -> momoka
else -> null
}
}
}

View File

@@ -0,0 +1,39 @@
package com.example.curation_train_app
import com.example.curation_train_app.ai.PromptBuilder
import com.example.curation_train_app.ai.AiClient
private const val API_KEY = "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"
class CharacterReplyManager {
private val responders = mapOf(
"reimu" to ReimuResponder(),
"akane" to AkaneResponder(),
"marisa" to MarisaResponder(),
"sayaka" to SayakResponder(),
"sanae" to SanaeResponder(),
"momoka" to MomokaResponder(),
"flandre" to flandreResponder(),
"hiyori" to HiyoriResponder()
// ← 6人追加予定
)
fun reply(characterId: String, info: String, type: InfoType): String {
val responder = responders[characterId]
?: return "キャラが見つかりません。"
return responder.respond(info, type)
}
fun replyWithAI(characterId: String, info: String, type: InfoType): String {
val character = CharacterProfiles.getProfile(characterId)
?: return "(エラー:キャラが不明です)"
val prompt = PromptBuilder.buildPrompt(character, info, type)
val ai = AiClient(API_KEY)
return ai.requestCharacterReply(prompt)
}
}

View File

@@ -0,0 +1,6 @@
package com.example.curation_train_app
interface CharacterResponder {
val profile: CharacterProfiles.CharacterProfile
fun respond(info: String, type: InfoType): String
}

View File

@@ -0,0 +1,18 @@
package com.example.curation_train_app
object DummyLines {
fun linesOf(region: String): List<String> {
return when (region) {
"北海道" -> listOf("JR北海道全線", "札幌市営地下鉄", "道南いさりび鉄道")
"東北" -> listOf("JR東日本東北", "仙台市地下鉄", "青い森鉄道")
"関東" -> listOf("JR東日本関東", "東京メトロ", "京急", "東急", "小田急", "京王")
"中部" -> listOf("JR東海", "名鉄", "近鉄(名古屋)")
"関西" -> listOf("JR西日本関西", "大阪メトロ", "阪急", "阪神", "京阪", "南海", "近鉄(関西)")
"中国" -> listOf("JR西日本中国", "広島電鉄", "一畑電車")
"四国" -> listOf("JR四国")
"九州" -> listOf("JR九州", "西鉄")
else -> listOf("該当する路線がありません")
}
}
}

View File

@@ -0,0 +1,12 @@
package com.example.curation_train_app
import android.os.Bundle
import androidx.activity.ComponentActivity
class FilterSettingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter_setting)
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class HiyoriResponder : CharacterResponder {
override val profile = CharacterProfiles.hiyori
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延しているみたい…焦らずに、少し余裕を持って動けるといいね。"
InfoType.REVISION ->
"ダイヤが変わるんだね…。知らないと戸惑うから、確認しておくと安心だよ。"
InfoType.EVENT ->
"イベント列車…!楽しそうだね。時間が合えば、ちょっと見に行ってみたいな。"
else ->
"気になったことがあるの?よかったら教えてね。いっしょに見てみるよ。"
}
}
}

View File

@@ -0,0 +1,28 @@
package com.example.curation_train_app
object InfoClassifier {
fun classify(text: String): InfoType {
val t = text.lowercase()
return when {
// 遅延系
t.contains("遅延") || t.contains("遅れ") || t.contains("delay") ->
InfoType.DELAY
// ダイヤ改正 / 運転変更
t.contains("ダイヤ") || t.contains("時刻表") || t.contains("運転変更") ->
InfoType.REVISION
// 注意報・警報・天候・安全
t.contains("強風") || t.contains("大雨") ||
t.contains("警報") || t.contains("注意喚起") ||
t.contains("事故") || t.contains("運休") ->
InfoType.WEATHER
// その他
else -> InfoType.OTHER
}
}
}

View File

@@ -0,0 +1,12 @@
package com.example.curation_train_app
enum class InfoType {
DELAY, // 遅延
SUSPENSION, // 運休
REVISION, // ダイヤ改正
CONSTRUCTION, // 工事
EVENT, // イベント・臨時列車
WEATHER, // 天候影響
NEW_TRAIN, // 新型車両関連
OTHER // 分類できない場合
}

View File

@@ -0,0 +1,52 @@
package com.example.curation_train_app
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import android.content.Intent
import android.widget.Button
class LinePriorityActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_line_priority)
val btnOpenFilter = findViewById<Button>(R.id.btnOpenFilter)
btnOpenFilter.setOnClickListener {
val intent = Intent(this, FilterSettingActivity::class.java)
startActivity(intent)
}
val lineName = intent.getStringExtra("line_name") ?: "不明な路線"
val selectedLine = findViewById<TextView>(R.id.textSelectedLine)
selectedLine.text = "選択された路線:$lineName"
// 優先度6枠まとめて取得
val box1 = findViewById<TextView>(R.id.priority1)
val box2 = findViewById<TextView>(R.id.priority2)
val box3 = findViewById<TextView>(R.id.priority3)
val box4 = findViewById<TextView>(R.id.priority4)
val box5 = findViewById<TextView>(R.id.priority5)
val box6 = findViewById<TextView>(R.id.priority6)
val boxes = listOf(box1, box2, box3, box4, box5, box6)
// ▼▼ 空き枠を探して路線名をセット ▼▼
for (box in boxes) {
val t = box.text.toString()
// 初期状態1〜6または空欄の場合は代入
if (t == "1" || t == "2" || t == "3" ||
t == "4" || t == "5" || t == "6" ||
t.isBlank()
) {
box.text = lineName
break
}
}
// ▲▲ 空き枠を探してセット ▲▲
}
}

View File

@@ -0,0 +1,40 @@
package com.example.curation_train_app
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class LineSelectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_line_select)
// ★ 地域名を受け取る
val region = intent.getStringExtra("region") ?: "未指定"
// ★ タイトル反映
val title = findViewById<TextView>(R.id.textRegionTitle)
title.text = "◆ 選択されている地域:$region"
// ★ 該当地域の路線リストを取得あとでDBやJSONに移動予定
val lines = DummyLines.linesOf(region)
// ★ リスト表示
val recycler = findViewById<RecyclerView>(R.id.recyclerLines)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = LineSelectAdapter(lines) { lineName ->
openPriorityScreen(lineName)
}
}
// 優先度設定画面へ遷移
private fun openPriorityScreen(line: String) {
val intent = Intent(this, LinePriorityActivity::class.java)
intent.putExtra("line_name", line)
startActivity(intent)
}
}

View File

@@ -0,0 +1,31 @@
package com.example.curation_train_app
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class LineSelectAdapter(
private val items: List<String>,
private val onClick: (String) -> Unit
) : RecyclerView.Adapter<LineSelectAdapter.ViewHolder>() {
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
val text: TextView = v.findViewById(R.id.textLineName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_line_name, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val name = items[position]
holder.text.text = name
holder.itemView.setOnClickListener { onClick(name) }
}
}

View File

@@ -4,33 +4,120 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.content.Intent
import android.widget.LinearLayout
import android.view.View
import android.widget.ImageView
import android.view.ViewGroup
import com.example.curation_train_app.ai.AiClient
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// ここで R.id.recyclerNews を使う val btn = findViewById<Button>(R.id.btnTestReply)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerNews) val inputText = findViewById<EditText>(R.id.inputText)
val inputCharacter = findViewById<EditText>(R.id.inputCharacter)
val textResult = findViewById<TextView>(R.id.textTestResult)
recyclerView.layoutManager = LinearLayoutManager(this) btn.setOnClickListener {
val text = inputText.text.toString()
val charId = inputCharacter.text.toString()
val newsList: List<NewsItem> = listOf( val type = InfoClassifier.classify(text)
NewsItem(
company = "△△電鉄",
title = "○○線で新型車両がデビューします!",
body = "2025年12月より○○線で新型車両が営業運転を開始します。ダイヤ変更にご注意ください。",
imageRes = R.drawable.ic_launcher_foreground
),
NewsItem(
company = "JR◆◆",
title = "○○線で駅ホーム延伸工事を実施",
body = "工事期間中は一部列車で停車位置が変更となります。係員の案内に従ってご利用ください。",
imageRes = R.drawable.ic_launcher_foreground
)
// …以下、必要な分だけ続ける
)
recyclerView.adapter = NewsAdapter(newsList) Thread {
val reply = CharacterReplyManager().replyWithAI(charId, text, type)
runOnUiThread {
val commentView = findViewById<TextView>(R.id.textCharacterComment)
val cleanText = if (reply.trim().startsWith("{") || reply.trim().startsWith("[")) {
AiClient(apiKey = "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").extractText(json = reply)
} else {
reply
}
commentView.text = cleanText
}
}.start()
}
loadRss()
// ▼ リリース枠(表示テスト) ▼
val releaseBox = findViewById<LinearLayout>(R.id.layoutRelease)
val releaseTitle = findViewById<TextView>(R.id.textReleaseTitle)
val releaseBody = findViewById<TextView>(R.id.textReleaseBody)
releaseTitle.text = "JR西日本新着情報"
releaseBody.text = "最新リリースがここに入る"
releaseBox.visibility = View.VISIBLE
// ▲ リリース枠 ▲
// ▼ 地域セレクト画面へ ▼
val btnOpenRegion = findViewById<TextView>(R.id.textFavorite)
btnOpenRegion.setOnClickListener {
val intent = Intent(this, RegionSelectActivity::class.java)
startActivity(intent)
}
// ▲ 地域セレクト画面へ ▲
// ▼ キャラ画像の初期配置 ▼
applyCharacterLayoutDefault()
}
// ▼ キャラ画像の位置調整関数 ▼
private fun applyCharacterLayoutDefault() {
val img = findViewById<ImageView>(R.id.imageCharacter)
val scale = resources.displayMetrics.density
val widthDp = 110
val heightDp = 110
val offsetXdp = -8
val offsetYdp = -18
img.layoutParams.width = (widthDp * scale).toInt()
img.layoutParams.height = (heightDp * scale).toInt()
val lp = img.layoutParams as ViewGroup.MarginLayoutParams
lp.marginStart = (offsetXdp * scale).toInt()
lp.topMargin = (offsetYdp * scale).toInt()
img.layoutParams = lp
img.requestLayout()
}
// ▼ RSS 読み込み ▼
private fun loadRss() {
Thread {
try {
val api = RssApi()
val feed = api.loadRss()
val list = feed?.channel?.items?.map { item ->
NewsItem(
company = "JR西日本",
title = item.title ?: "",
body = item.description ?: ""
)
} ?: emptyList()
runOnUiThread {
val recyclerNews = findViewById<RecyclerView>(R.id.recyclerNews)
recyclerNews.adapter = NewsAdapter(list)
}
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
} }
} }

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class MarisaResponder : CharacterResponder {
override val profile = CharacterProfiles.marisa
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延か。原因が気になるところだぜ。無理せず、回避ルートを考えるのもアリだな。"
InfoType.REVISION ->
"ダイヤ改正だな。運用の流れが結構変わる時期なんだぜ。新しい列車の動きも要チェックだ。"
InfoType.EVENT ->
"イベント列車か。撮るならポイント選びが肝心だぜ。準備して臨むといいと思うぜ。"
else ->
"ふむ?何か気になることがあるんだな。他にも情報があれば教えてくれよ。"
}
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class MomokaResponder : CharacterResponder{
override val profile = CharacterProfiles.momoka
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ…遅れてるの?大丈夫かな…。のんびり落ち着いていこうねっ。"
InfoType.REVISION ->
"ダイヤが変わるんだね!ちょっとドキドキしちゃうよ〜。"
InfoType.EVENT ->
"イベント列車!?わぁ〜楽しそう!時間が合ったら絶対見たいなっ!"
else ->
"気になることがあったんだね。よかったら、もう少し教えてねっ!"
}
}
}

View File

@@ -4,6 +4,6 @@ data class NewsItem(
val company: String, val company: String,
val title: String, val title: String,
val body: String, val body: String,
val imageRes: Int
) )

View File

@@ -0,0 +1,29 @@
package com.example.curation_train_app
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import android.widget.Button
class RegionSelectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_region_select)
fun go(region: String) {
val intent = Intent(this, LineSelectActivity::class.java)
intent.putExtra("region", region)
startActivity(intent)
}
findViewById<Button>(R.id.btnNorth).setOnClickListener { go("北海道") }
findViewById<Button>(R.id.btnTohoku).setOnClickListener { go("東北") }
findViewById<Button>(R.id.btnKanto).setOnClickListener { go("関東") }
findViewById<Button>(R.id.btnChubu).setOnClickListener { go("中部") }
findViewById<Button>(R.id.btnKansai).setOnClickListener { go("関西") }
findViewById<Button>(R.id.btnChugoku).setOnClickListener { go("中国") }
findViewById<Button>(R.id.btnShikoku).setOnClickListener { go("四国") }
findViewById<Button>(R.id.btnKyushu).setOnClickListener { go("九州") }
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class ReimuResponder : CharacterResponder {
override val profile = CharacterProfiles.reimu
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延みたいね。急がなければ、少し余裕を持って動くのがいいわよ。落ち着いていきましょう。"
InfoType.REVISION ->
"ダイヤ改正があるみたいね。大きな変化がある時期だから、いつもより確認しておくと安心よ。"
InfoType.EVENT ->
"イベント列車が走るみたいね。せっかくだから、時間が合えば見に行くと楽しいわよ。"
else ->
"気になったのね。内容をもう少し教えてくれれば、ちゃんと見てみるわよ。"
}
}
}

View File

@@ -0,0 +1,20 @@
package com.example.curation_train_app
import retrofit2.Retrofit
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
class RssApi {
private val retrofit = Retrofit.Builder()
.baseUrl("https://www.westjr.co.jp/press/article/index.xml")
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
private val client = retrofit.create(RssClient::class.java)
fun loadRss(): RssFeed? {
val response = client.getFeed().execute()
return response.body()
}
}

View File

@@ -0,0 +1,9 @@
package com.example.curation_train_app
import retrofit2.Call
import retrofit2.http.GET
interface RssClient {
@GET("article/index.xml")
fun getFeed(): Call<RssFeed>
}

View File

@@ -0,0 +1,32 @@
package com.example.curation_train_app
import org.simpleframework.xml.Element
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
@Root(name = "rss", strict = false)
data class RssFeed(
@field:Element(name = "channel")
var channel: RssChannel? = null
)
@Root(name = "channel", strict = false)
data class RssChannel(
@field:ElementList(name = "item", inline = true, required = false)
var items: List<RssItem>? = null
)
@Root(name = "item", strict = false)
data class RssItem(
@field:Element(name = "title", required = false)
var title: String = "",
@field:Element(name = "description", required = false)
var description: String = "",
@field:Element(name = "link", required = false)
var link: String = "",
@field:Element(name = "pubDate", required = false)
var pubDate: String = ""
)

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class SanaeResponder : CharacterResponder {
override val profile = CharacterProfiles.sanae
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延が出ているようですね。無理のない範囲で、時間に余裕を持って動かれると良いと思いますよ。"
InfoType.REVISION ->
"ダイヤ改正が予定されているみたいですね。列車の動きが変わるので、ご注意くださいね。"
InfoType.EVENT ->
"イベント列車ですか。季節感や特別感があって素敵ですよね。機会があれば見に行きたいですね。"
else ->
"気になる点がありましたか?よければ詳しく教えてくださいね。"
}
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class SayakResponder : CharacterResponder {
override val profile = CharacterProfiles.sayaka
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延が出ているようね。急がないのであれば、少し余裕を持って動くと安心よ。"
InfoType.REVISION ->
"ダイヤ改正の情報ね。列車の動きが変わることもあるから、事前に確認しておくと良いわ。"
InfoType.EVENT ->
"イベント列車が走るみたいね。珍しい機会だし、時間が合えば見てみるのもいいと思うわよ。"
else ->
"気になる点があるのね。もう少し詳しく教えてくれれば、整理してお話しできると思うわ。"
}
}
}

View File

@@ -0,0 +1,54 @@
package com.example.curation_train_app.ai
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
class AiClient(private val apiKey: String) {
private val client = OkHttpClient()
fun requestCharacterReply(prompt: String): String {
val url = "https://api.openai.com/v1/responses"
val json = JSONObject().apply {
put("model", "gpt-4.1-mini") // ⭐ ChatGPT 5.2 mini 相当
put("input", prompt) // ⭐ messages ではなく input
put("max_output_tokens", 150)
}
val body = json.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(url)
.post(body)
.addHeader("Authorization", "Bearer $apiKey")
.build()
client.newCall(request).execute().use { response ->
val responseBody = response.body?.string()
?: return "AIエラー応答なし"
val obj = JSONObject(responseBody)
// ⭐ 新APIは “output” 配列の中に text が入っている
val outputArray = obj.getJSONArray("output")
val outputObj = outputArray.getJSONObject(0)
// ⭐ content だけ取り出す
val text = outputObj.getString("content")
return text
}
}
fun extractText(json: String): String {
return try {
val obj = org.json.JSONObject(json)
obj.optString("text", json)
} catch (_: Exception) {
json
}
}
}

View File

@@ -0,0 +1,33 @@
package com.example.curation_train_app.ai
import com.example.curation_train_app.CharacterProfiles
import com.example.curation_train_app.InfoType
object PromptBuilder {
fun buildPrompt(
character: CharacterProfiles.CharacterProfile,
info: String,
type: InfoType
): String {
return """
あなたは以下のキャラクターになりきって返答してください。
【キャラ設定】
名前:${character.name}
話し方:${character.speakingStyle}
性格:${character.personality}
知識レベル:${character.knowledgeLevel}
【状況】
鉄道情報の種類:$type
内容:$info
【出力条件】
・キャラになりきる
・短く、自然に、感情をほんの少し込める
・ユーザーへ助言する感じで
""".trimIndent()
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class flandreResponder : CharacterResponder {
override val profile = CharacterProfiles.flan
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ、遅れてるの?大丈夫かな?ちょっと心配だよ!"
InfoType.REVISION ->
"ダイヤが変わっちゃうんだ!なんだかワクワクしてきちゃうかも!"
InfoType.EVENT ->
"イベント列車!?絶対楽しいやつじゃん!時間が合えば見に行きたいのだー!"
else ->
"なになに?気になることがあるの?フランに教えてほしいのだ!"
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="情報フィルター設定(仮)"
android:textSize="20sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/checkOfficial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="公式の情報のみ" />
<CheckBox
android:id="@+id/checkDelay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="運行・遅延情報" />
<CheckBox
android:id="@+id/checkOuting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お出かけ関連情報" />
</LinearLayout>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E8F8E5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- タイトル -->
<TextView
android:id="@+id/textSelectedLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="選択された路線:"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<!-- 優先度の枠16 -->
<TextView
android:id="@+id/priority1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="1"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="2"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority3"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="3"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority4"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="4"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority5"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="5"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority6"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="6"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnOpenFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="情報フィルターへ"
android:layout_marginTop="16dp"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical"
android:background="#E8F8E5">
<TextView
android:id="@+id/textRegionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="◆ 選択されている地域:"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="12dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerLines"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="4dp" />
<!-- ▼▼ お知らせ設定エリア ▼▼ -->
<LinearLayout
android:id="@+id/noticeSettingBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:layout_marginTop="16dp"
android:minHeight="120dp"
android:background="@drawable/comment_background_white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="■ お知らせ設定"
android:textStyle="bold"
android:textSize="14sp"
android:paddingBottom="8dp"/>
<CheckBox
android:id="@+id/checkNoticeTrain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="運行・遅延情報"/>
<CheckBox
android:id="@+id/checkNoticeOuting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お出かけ関連情報"/>
<CheckBox
android:id="@+id/checkNoticeCampaign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="キャンペーン・お楽しみ情報"/>
<CheckBox
android:id="@+id/checkNoticeService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="サービス・アプリ関連"/>
</LinearLayout>
<!-- ▲▲ キャラ選択エリア ▲▲ -->
<!-- ▲▲ お知らせ設定エリア ▲▲ -->
</LinearLayout>

View File

@@ -1,18 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:orientation="vertical" android:orientation="vertical"
android:background="#EAF7EF"> android:background="#EAF7EF">
<!-- タイトル行 -->
<!-- タイトル行(白いカード背景) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp" android:padding="16dp"
android:background="#EAF7EF"> android:layout_margin="12dp"
android:background="@drawable/comment_background_white"
android:elevation="2dp">
<TextView <TextView
android:id="@+id/textTitle" android:id="@+id/textTitle"
@@ -20,7 +24,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="○○線:平常運転" android:text="○○線:平常運転"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" /> android:textStyle="bold"
android:paddingBottom="8dp" />
<TextView <TextView
android:id="@+id/textFavorite" android:id="@+id/textFavorite"
@@ -28,13 +33,49 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="お気に入り登録・設定" android:text="お気に入り登録・設定"
android:gravity="center" android:gravity="center"
android:paddingTop="8dp" android:paddingTop="6dp"
android:paddingBottom="8dp" android:paddingBottom="6dp"
android:background="#F0F0F0" android:background="#F7F7F7"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
<!-- 運行情報カード(白背景) -->
<!-- ▼▼ リリース情報ボックス1つだけ ▼▼ -->
<!-- リリース情報カード(白背景) -->
<LinearLayout
android:id="@+id/layoutRelease"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:layout_margin="12dp"
android:background="@drawable/comment_background_white"
android:elevation="2dp"
android:visibility="visible">
<TextView
android:id="@+id/textReleaseTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="リリース情報"
android:textStyle="bold"
android:textSize="12sp" />
<TextView
android:id="@+id/textReleaseBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ここにリリース本文が入ります"
android:textSize="13sp"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- ▲▲ リリース情報ボックス ▲▲ -->
<!-- 運行情報カード -->
<LinearLayout <LinearLayout
android:id="@+id/layoutNotice" android:id="@+id/layoutNotice"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -43,7 +84,8 @@
android:padding="12dp" android:padding="12dp"
android:layout_margin="12dp" android:layout_margin="12dp"
android:background="@drawable/comment_background_white" android:background="@drawable/comment_background_white"
android:elevation="2dp"> android:elevation="2dp"
android:visibility="gone">
<TextView <TextView
android:id="@+id/textNoticeTitle" android:id="@+id/textNoticeTitle"
@@ -62,7 +104,8 @@
android:layout_marginTop="4dp" /> android:layout_marginTop="4dp" />
</LinearLayout> </LinearLayout>
<!-- 運行情報一覧(スクロール) -->
<!-- 運行情報リスト -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerNews" android:id="@+id/recyclerNews"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -70,33 +113,88 @@
android:layout_weight="1" android:layout_weight="1"
android:padding="8dp" /> android:padding="8dp" />
<!-- 下部:一言 & あかね -->
<!-- ▼▼ キャラ応答テストUI ▼▼ -->
<LinearLayout <LinearLayout
android:id="@+id/testArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="#EEEEEE">
<EditText
android:id="@+id/inputText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="テスト用の文章を入力" />
<EditText
android:id="@+id/inputCharacter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="キャラIDreimu / akane" />
<Button
android:id="@+id/btnTestReply"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="キャラにしゃべらせる" />
<TextView
android:id="@+id/textTestResult"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="※結果がここに表示されます"
android:paddingTop="8dp" />
</LinearLayout>
<!-- ▲▲ キャラ応答テストUI ▲▲ -->
<!-- ▼ 下部キャラ表示 ▼ -->
<!-- ▼ 下部キャラ表示 ▼ -->
<FrameLayout
android:id="@+id/bottomArea" android:id="@+id/bottomArea"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:padding="16dp">
android:padding="16dp"
android:gravity="bottom">
<!-- コメントテキスト(左側) -->
<TextView <TextView
android:id="@+id/textCharacterComment" android:id="@+id/textCharacterComment"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/comment_background_white" android:background="@drawable/comment_background_white"
android:padding="8dp" android:padding="13dp"
android:text="ちょっとした一言(例:今日から●●線のダイヤが変わってるから気をつけてね!)" android:text="ちょっとした一言"
android:textSize="13sp" /> android:textSize="13sp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="110dp"
android:translationX="-8dp"
android:translationY="0dp" />
<!-- キャラ(右下固定・サイズ自由) -->
<ImageView <ImageView
android:id="@+id/imageCharacter" android:id="@+id/imageCharacter"
android:layout_width="80dp" android:layout_width="280dp"
android:layout_height="140dp" android:layout_height="300dp"
android:layout_marginStart="8dp"
android:src="@drawable/akane_01_icon" android:src="@drawable/akane_01_icon"
android:adjustViewBounds="true" android:adjustViewBounds="false"
android:scaleType="fitCenter" /> android:scaleType="fitCenter"
</LinearLayout> android:layout_gravity="bottom|end"
android:translationX="8dp"
android:translationY="-4dp" />
</FrameLayout>
<!-- ▲ 下部キャラ表示 ▲ -->
<!-- ▲ 下部キャラ表示 ▲ -->
</LinearLayout> </LinearLayout>
<!-- 下部キャラ表示 -->

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E8F8E5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- タイトル(お気に入り路線登録) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お気に入り路線登録"
android:textSize="20sp"
android:textStyle="bold"
android:paddingBottom="12dp"/>
<!-- 8地域グリッド -->
<GridLayout
android:id="@+id/gridRegions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="4"
android:alignmentMode="alignMargins"
android:columnOrderPreserved="false"
android:useDefaultMargins="true">
<Button
android:id="@+id/btnNorth"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="北海道"
android:textSize="18sp"/>
<Button
android:id="@+id/btnTohoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="東北"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKanto"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="関東"
android:textSize="18sp"/>
<Button
android:id="@+id/btnChubu"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="中部"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKansai"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="関西"
android:textSize="18sp"/>
<Button
android:id="@+id/btnChugoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="中国"
android:textSize="18sp"/>
<Button
android:id="@+id/btnShikoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="四国"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKyushu"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="九州"
android:textSize="18sp"/>
</GridLayout>
<!-- ▼▼ キャラ選択エリア ▼▼ -->
<LinearLayout
android:id="@+id/layoutCharacters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/comment_background_white"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ナビゲーター設定"
android:textSize="16sp"
android:textStyle="bold"
android:paddingBottom="8dp"/>
<!-- 8キャラボタン -->
<Button
android:id="@+id/btnReimu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔴 霊夢(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnMarisa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟡 魔理沙(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnFrandle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟠 フラン(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnSanae"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟢 早苗(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnAkane"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟨 あかね(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnMomoka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🍑 ももか(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnSayaka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟦 さやか(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnHiyori"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟧 ひより(キャラ紹介)"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textLineName"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textSize="16sp"
android:background="#FFFFFF"
android:text="路線名"
android:layout_marginBottom="6dp"/>

View File

@@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">curation_train_app</string> <string name="app_name">curation_train_app</string>
<string name="openai_key">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</string>
</resources> </resources>